This repository has been archived by the owner on May 12, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 296
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #511 from tuncer/memo
Add and use memoization server
- Loading branch information
Showing
4 changed files
with
310 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,294 @@ | ||
%%% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- | ||
%%% ex: ft=erlang ts=4 sw=4 et | ||
%%% | ||
%%%------------------------------------------------------------------- | ||
%%% @author Tuncer Ayaz | ||
%%% @copyright 2015, Tuncer Ayaz | ||
%%% @doc | ||
%%% memoization server | ||
%%% @end | ||
%%%------------------------------------------------------------------- | ||
%%% | ||
%%% Copyright (c) 2015 Tuncer Ayaz | ||
%%% | ||
%%% Permission to use, copy, modify, and/or distribute this software | ||
%%% for any purpose with or without fee is hereby granted, provided | ||
%%% that the above copyright notice and this permission notice appear | ||
%%% in all copies. | ||
%%% | ||
%%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL | ||
%%% WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED | ||
%%% WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE | ||
%%% AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR | ||
%%% CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM | ||
%%% LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, | ||
%%% NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN | ||
%%% CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | ||
|
||
%% rebar-specific modifications: | ||
%% 1. rename to rmemo.erl | ||
%% 2. add support for R13 (see ets_tab/0) | ||
|
||
-module(rmemo). | ||
|
||
-behaviour(gen_server). | ||
|
||
%% API | ||
-export( | ||
[ | ||
start/0, | ||
start_link/0, | ||
stop/0, | ||
call/2, | ||
call/3 | ||
]). | ||
|
||
%% gen_server callbacks | ||
-export( | ||
[ | ||
init/1, | ||
handle_call/3, | ||
handle_cast/2, | ||
handle_info/2, | ||
terminate/2, | ||
code_change/3 | ||
]). | ||
|
||
-define(SERVER, ?MODULE). | ||
-define(TABLE, ?MODULE). | ||
|
||
-record(state, | ||
{ | ||
ets_tab :: ets:tab() | ||
}). | ||
|
||
%%%=================================================================== | ||
%%% API | ||
%%%=================================================================== | ||
|
||
|
||
%%-------------------------------------------------------------------- | ||
%% @doc | ||
%% Start the server | ||
%% @end | ||
%%-------------------------------------------------------------------- | ||
-type reason() :: term(). | ||
-type error() :: {error, reason()}. | ||
-type start_res() :: {ok, pid()} | 'ignore' | error(). | ||
-spec start() -> start_res(). | ||
start() -> | ||
gen_server:start({local, ?SERVER}, ?MODULE, [], []). | ||
|
||
%%-------------------------------------------------------------------- | ||
%% @doc | ||
%% Start the server | ||
%% @end | ||
%%-------------------------------------------------------------------- | ||
-type start_link_res() :: start_res(). | ||
-spec start_link() -> start_link_res(). | ||
start_link() -> | ||
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). | ||
|
||
%%-------------------------------------------------------------------- | ||
%% @doc | ||
%% Stop the server | ||
%% @end | ||
%%-------------------------------------------------------------------- | ||
stop() -> | ||
gen_server:cast(?SERVER, stop). | ||
|
||
%%-------------------------------------------------------------------- | ||
%% @doc | ||
%% Call function and memoize result | ||
%% | ||
%% Instead of | ||
%% | ||
%% <code>Res = Fun(A1, A2, [List1])</code> | ||
%% | ||
%% you call | ||
%% | ||
%% <code>Res = memo:call(Fun, [A1, A2, [List1]])</code> | ||
%% | ||
%% or instead of | ||
%% | ||
%% <code> | ||
%% Res = mod:expensive_function(A1, A2, [List1]) | ||
%% </code> | ||
%% | ||
%% you call | ||
%% | ||
%% <code> | ||
%% Res = memo:call(fun mod:expensive_function/3, [A1, A2, [List1]]) | ||
%% </code> | ||
%% | ||
%% and any subsequent call will fetch the cached result and avoid the | ||
%% computation. | ||
%% | ||
%% This is of course only useful for expensive computations that are | ||
%% known to produce the same result given same arguments. It's worth | ||
%% mentioning that your call should be side-effect free, as naturally | ||
%% those won't be replayed. | ||
%% | ||
%% @end | ||
%%-------------------------------------------------------------------- | ||
-type fun_args() :: list(). | ||
-spec call(fun(), fun_args()) -> term(). | ||
call(F, A) -> | ||
call_1({F, A}). | ||
|
||
%%-------------------------------------------------------------------- | ||
%% @doc | ||
%% Call function and memoize result | ||
%% | ||
%% Instead of | ||
%% | ||
%% <code>Res = mod:expensive_function(A1, A2, [List1])</code> | ||
%% | ||
%% you call | ||
%% | ||
%% <code>Res = memo:call(mod, expensive_function, [A1, A2, [List1]])</code> | ||
%% | ||
%% and any subsequent call will fetch the cached result and avoid the | ||
%% computation. | ||
%% | ||
%% This is of course only useful for expensive computations that are | ||
%% known to produce the same result given same arguments. It's worth | ||
%% mentioning that your call should be side-effect free, as naturally | ||
%% those won't be replayed.%% | ||
%% | ||
%% @end | ||
%%-------------------------------------------------------------------- | ||
%% fun() is not just the name of a fun, so we define an alias for | ||
%% atom() for call(M, F, A). | ||
-type fun_name() :: atom(). | ||
-spec call(module(), fun_name(), fun_args()) -> term(). | ||
call(M, F, A) when is_list(A) -> | ||
call_1({M, F, A}). | ||
|
||
%%%=================================================================== | ||
%%% gen_server callbacks | ||
%%%=================================================================== | ||
|
||
%%-------------------------------------------------------------------- | ||
%% @private | ||
%% @doc | ||
%% Initialize the server | ||
%% @end | ||
%%-------------------------------------------------------------------- | ||
init(_) -> | ||
{ok, | ||
#state{ | ||
ets_tab = ets_tab() | ||
} | ||
}. | ||
|
||
-spec ets_tab() -> ets:tab(). | ||
ets_tab() -> | ||
ErtsApp = filename:join(code:lib_dir(erts, ebin), "erts.app"), | ||
Concurrency = | ||
%% If erts.app exists, we run on at least R14. That means we | ||
%% can use ets read_concurrency. | ||
%% TODO: Remove and revert to vanilla memo.erl from | ||
%% https://github.com/tuncer/memo once we require at least | ||
%% R14B and drop support for R13. | ||
case filelib:is_regular(ErtsApp) of | ||
true -> | ||
[{read_concurrency, true}]; | ||
false -> | ||
[] | ||
end, | ||
ets:new( | ||
?TABLE, | ||
[ | ||
named_table, | ||
protected, | ||
set | ||
] | ||
++ Concurrency | ||
). | ||
|
||
%%-------------------------------------------------------------------- | ||
%% @private | ||
%% @doc | ||
%% Handle call messages | ||
%% @end | ||
%%-------------------------------------------------------------------- | ||
handle_call({save, Key, Res}, _From, State) -> | ||
{reply, save(Key, Res), State}; | ||
handle_call(_Request, _From, State) -> | ||
{reply, {error, undefined_call}, State}. | ||
|
||
%%-------------------------------------------------------------------- | ||
%% @private | ||
%% @doc | ||
%% Handle cast messages | ||
%% @end | ||
%%-------------------------------------------------------------------- | ||
handle_cast(stop, State) -> | ||
{stop, normal, State}; | ||
handle_cast(_Msg, State) -> | ||
{noreply, State}. | ||
|
||
%%-------------------------------------------------------------------- | ||
%% @private | ||
%% @doc | ||
%% Handle all non call/cast messages | ||
%% @end | ||
%%-------------------------------------------------------------------- | ||
handle_info(_Info, State) -> | ||
{noreply, State}. | ||
|
||
%%-------------------------------------------------------------------- | ||
%% @private | ||
%% @doc | ||
%% This function is called by a gen_server when it is about to | ||
%% terminate. It should be the opposite of Module:init/1 and do any | ||
%% necessary cleaning up. When it returns, the gen_server terminates | ||
%% with Reason. The return value is ignored. | ||
%% @end | ||
%%-------------------------------------------------------------------- | ||
terminate(_Reason, _State) -> | ||
ok. | ||
|
||
%%-------------------------------------------------------------------- | ||
%% @private | ||
%% @doc | ||
%% Convert process state when code is changed | ||
%% @end | ||
%%-------------------------------------------------------------------- | ||
code_change(_OldVsn, State, _Extra) -> | ||
{ok, State}. | ||
|
||
%%%=================================================================== | ||
%%% Internal functions | ||
%%%=================================================================== | ||
|
||
-type call() :: {module(), fun_name(), fun_args()} | {fun(), fun_args()}. | ||
-spec call_1(call()) -> term(). | ||
call_1(Call) -> | ||
Key = key(Call), | ||
case ets:lookup(?TABLE, Key) of | ||
[] -> | ||
Res = apply(Call), | ||
true = gen_server:call(?SERVER, {save, Key, Res}, infinity), | ||
Res; | ||
[{Key, Mem}] -> | ||
Mem | ||
end. | ||
|
||
-type key_args() :: call(). | ||
-type key() :: non_neg_integer(). | ||
-spec key(key_args()) -> key(). | ||
key(Call) -> | ||
erlang:phash2(Call). | ||
|
||
-spec apply(call()) -> term(). | ||
apply({F, A}) -> | ||
erlang:apply(F, A); | ||
apply({M, F, A}) -> | ||
erlang:apply(M, F, A). | ||
|
||
-type val() :: term(). | ||
-spec save(key(), val()) -> true. | ||
save(K, V) -> | ||
ets:insert(?TABLE, {K, V}). |