Packages

Elixir-inspired standard library modules for Erlang

Current section

Files

Jump to
ex_stdlib src agent.erl
Raw

src/agent.erl

%%% @doc
%%% Agents are a simple abstraction around state.
%%%
%%% Often in Erlang there is a need to share or store state that
%%% must be accessed from different processes or by the same process
%%% at different points in time.
%%%
%%% The agent module provides a basic server implementation that
%%% allows state to be retrieved and updated via a simple API.
%%%
%%% == Examples ==
%%%
%%% For example, the following agent implements a counter:
%%%
%%% ```
%%% -module(counter).
%%% -export([start_link/1, value/0, increment/0]).
%%%
%%% start_link(InitialValue) ->
%%% agent:start_link(fun() -> InitialValue end, [{name, ?MODULE}]).
%%%
%%% value() ->
%%% agent:get(?MODULE, fun(State) -> State end).
%%%
%%% increment() ->
%%% agent:update(?MODULE, fun(State) -> State + 1 end).
%%% '''
%%%
%%% Usage would be:
%%%
%%% ```
%%% {ok, Pid} = counter:start_link(0).
%%% 0 = counter:value().
%%% ok = counter:increment().
%%% ok = counter:increment().
%%% 2 = counter:value().
%%% '''
%%%
%%% Thanks to the agent server process, the counter can be safely incremented
%%% concurrently.
%%%
%%% Agents provide a segregation between the client and server APIs (similar to
%%% gen_servers). In particular, the functions passed as arguments to the calls to
%%% agent functions are invoked inside the agent (the server). This distinction
%%% is important because you may want to avoid expensive operations inside the
%%% agent, as they will effectively block the agent until the request is
%%% fulfilled.
%%%
%%% Consider these two examples:
%%%
%%% ```
%%% %% Compute in the agent/server
%%% get_something(Agent) ->
%%% agent:get(Agent, fun(State) -> do_something_expensive(State) end).
%%%
%%% %% Compute in the agent/client
%%% get_something(Agent) ->
%%% State = agent:get(Agent, fun(State) -> State end),
%%% do_something_expensive(State).
%%% '''
%%%
%%% The first function blocks the agent. The second function copies all the state
%%% to the client and then executes the operation in the client. One aspect to
%%% consider is whether the data is large enough to require processing in the server,
%%% at least initially, or small enough to be sent to the client cheaply. Another
%%% factor is whether the data needs to be processed atomically: getting the
%%% state and calling do_something_expensive(State) outside of the agent means
%%% that the agent's state can be updated in the meantime. This is specially
%%% important in case of updates as computing the new state in the client rather
%%% than in the server can lead to race conditions if multiple clients are trying
%%% to update the same state to different values.
%%%
%%% @end
-module(agent).
%% API
-export([start_link/1, start_link/2, start_link/4, start_link/5]).
-export([start/1, start/2, start/4, start/5]).
-export([get/2, get/3, get/5, get/6]).
-export([get_and_update/2, get_and_update/3, get_and_update/5, get_and_update/6]).
-export([update/2, update/3, update/5, update/6]).
-export([cast/2, cast/4]).
-export([stop/1, stop/2, stop/3]).
%% gen_server callbacks
-behaviour(gen_server).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
%% Types
-type on_start() :: {ok, pid()} | {error, {already_started, pid()} | term()}.
-type name() :: atom() | {global, term()} | {via, module(), term()}.
-type agent() :: pid() | {atom(), node()} | name().
-type state() :: term().
-type init_fun() :: fun(() -> state()).
-type get_fun() :: fun((state()) -> term()).
-type update_fun() :: fun((state()) -> state()).
-type get_and_update_fun() :: fun((state()) -> {term(), state()}).
-type timeout_ms() :: non_neg_integer() | infinity.
-export_type([on_start/0, name/0, agent/0, state/0]).
%%% @doc
%%% Starts an agent linked to the current process with the given function.
%%%
%%% This is often used to start the agent as part of a supervision tree.
%%%
%%% Once the agent is spawned, the given function Fun is invoked in the server
%%% process, and should return the initial agent state. Note that start_link/2
%%% does not return until the given function has returned.
%%%
%%% === Options ===
%%%
%%% The name option is used for registration. Valid values are:
%%% - {name, LocalName} where LocalName is an atom
%%% - {name, {global, GlobalName}} where GlobalName is any term
%%% - {name, {via, Module, ViaName}} for custom registration
%%%
%%% If the timeout option is present, the agent is allowed to spend at most
%%% the given number of milliseconds on initialization or it will be terminated
%%% and the start function will return {error, timeout}.
%%%
%%% If the debug option is present, the corresponding function in the
%%% sys module will be invoked.
%%%
%%% If the spawn_opt option is present, its value will be passed as options
%%% to the underlying process.
%%%
%%% === Return values ===
%%%
%%% If the server is successfully created and initialized, the function returns
%%% {ok, Pid}, where Pid is the PID of the server. If an agent with the
%%% specified name already exists, the function returns
%%% {error, {already_started, Pid}} with the PID of that process.
%%%
%%% If the given function callback fails, the function returns {error, Reason}.
%%% @end
-spec start_link(init_fun()) -> on_start().
start_link(Fun) when is_function(Fun, 0) ->
gen_server:start_link(?MODULE, Fun, []).
-spec start_link(init_fun(), [gen_server:start_opt()]) -> on_start().
start_link(Fun, Options) when is_function(Fun, 0) ->
case proplists:get_value(name, Options) of
undefined ->
gen_server:start_link(?MODULE, Fun, proplists:delete(name, Options));
Name ->
gen_server:start_link(Name, ?MODULE, Fun, proplists:delete(name, Options))
end.
%%% @doc
%%% Starts an agent linked to the current process.
%%%
%%% Same as start_link/2 but a module, function, and arguments are expected
%%% instead of an anonymous function; Fun in Module will be called with the
%%% given arguments Args to initialize the state.
%%% @end
-spec start_link(module(), atom(), [term()], [gen_server:start_opt()]) -> on_start().
start_link(Module, Fun, Args, Options) ->
gen_server:start_link(?MODULE, {Module, Fun, Args}, Options).
-spec start_link(name(), module(), atom(), [term()], [gen_server:start_opt()]) -> on_start().
start_link(Name, Module, Fun, Args, Options) ->
gen_server:start_link(Name, ?MODULE, {Module, Fun, Args}, Options).
%%% @doc
%%% Starts an agent process without links (outside of a supervision tree).
%%%
%%% See start_link/2 for more information.
%%% @end
-spec start(init_fun()) -> on_start().
start(Fun) when is_function(Fun, 0) ->
gen_server:start(?MODULE, Fun, []).
-spec start(init_fun(), [gen_server:start_opt()]) -> on_start().
start(Fun, Options) when is_function(Fun, 0) ->
case proplists:get_value(name, Options) of
undefined ->
gen_server:start(?MODULE, Fun, proplists:delete(name, Options));
Name ->
gen_server:start(Name, ?MODULE, Fun, proplists:delete(name, Options))
end.
%%% @doc
%%% Starts an agent without links with the given module, function, and arguments.
%%%
%%% See start_link/4 for more information.
%%% @end
-spec start(module(), atom(), [term()], [gen_server:start_opt()]) -> on_start().
start(Module, Fun, Args, Options) ->
gen_server:start(?MODULE, {Module, Fun, Args}, Options).
-spec start(name(), module(), atom(), [term()], [gen_server:start_opt()]) -> on_start().
start(Name, Module, Fun, Args, Options) ->
gen_server:start(Name, ?MODULE, {Module, Fun, Args}, Options).
%%% @doc
%%% Gets an agent value via the given anonymous function.
%%%
%%% The function Fun is sent to the Agent which invokes the function
%%% passing the agent state. The result of the function invocation is
%%% returned from this function.
%%%
%%% Timeout is an integer greater than zero which specifies how many
%%% milliseconds are allowed before the agent executes the function and returns
%%% the result value, or the atom infinity to wait indefinitely. If no result
%%% is received within the specified time, the function call fails and the caller
%%% exits.
%%% @end
-spec get(agent(), get_fun()) -> term().
get(Agent, Fun) when is_function(Fun, 1) ->
get(Agent, Fun, 5000).
-spec get(agent(), get_fun(), timeout_ms()) -> term().
get(Agent, Fun, Timeout) when is_function(Fun, 1) ->
gen_server:call(Agent, {get, Fun}, Timeout).
%%% @doc
%%% Gets an agent value via the given function.
%%%
%%% Same as get/3 but a module, function, and arguments are expected
%%% instead of an anonymous function. The state is added as first
%%% argument to the given list of arguments.
%%% @end
-spec get(agent(), module(), atom(), [term()], timeout_ms()) -> term().
get(Agent, Module, Fun, Args, Timeout) ->
gen_server:call(Agent, {get, {Module, Fun, Args}}, Timeout).
-spec get(agent(), module(), atom(), [term()], timeout_ms(), timeout_ms()) -> term().
get(Agent, Module, Fun, Args, Timeout, _GenTimeout) ->
gen_server:call(Agent, {get, {Module, Fun, Args}}, Timeout).
%%% @doc
%%% Gets and updates the agent state in one operation via the given anonymous
%%% function.
%%%
%%% The function Fun is sent to the Agent which invokes the function
%%% passing the agent state. The function must return a tuple with two
%%% elements, the first being the value to return (that is, the "get" value)
%%% and the second one being the new state of the agent.
%%%
%%% Timeout is an integer greater than zero which specifies how many
%%% milliseconds are allowed before the agent executes the function and returns
%%% the result value, or the atom infinity to wait indefinitely. If no result
%%% is received within the specified time, the function call fails and the caller
%%% exits.
%%% @end
-spec get_and_update(agent(), get_and_update_fun()) -> term().
get_and_update(Agent, Fun) when is_function(Fun, 1) ->
get_and_update(Agent, Fun, 5000).
-spec get_and_update(agent(), get_and_update_fun(), timeout_ms()) -> term().
get_and_update(Agent, Fun, Timeout) when is_function(Fun, 1) ->
case gen_server:call(Agent, {get_and_update, Fun}, Timeout) of
{error, {bad_return_value, Other}} ->
exit({{bad_return_value, Other}, {gen_server, call, [Agent, {get_and_update, Fun}]}});
Reply ->
Reply
end.
%%% @doc
%%% Gets and updates the agent state in one operation via the given function.
%%%
%%% Same as get_and_update/3 but a module, function, and arguments are expected
%%% instead of an anonymous function. The state is added as first
%%% argument to the given list of arguments.
%%% @end
-spec get_and_update(agent(), module(), atom(), [term()], timeout_ms()) -> term().
get_and_update(Agent, Module, Fun, Args, Timeout) ->
gen_server:call(Agent, {get_and_update, {Module, Fun, Args}}, Timeout).
-spec get_and_update(agent(), module(), atom(), [term()], timeout_ms(), timeout_ms()) -> term().
get_and_update(Agent, Module, Fun, Args, Timeout, _GenTimeout) ->
gen_server:call(Agent, {get_and_update, {Module, Fun, Args}}, Timeout).
%%% @doc
%%% Updates the agent state via the given anonymous function.
%%%
%%% The function Fun is sent to the Agent which invokes the function
%%% passing the agent state. The return value of Fun becomes the new
%%% state of the agent.
%%%
%%% This function always returns ok.
%%%
%%% Timeout is an integer greater than zero which specifies how many
%%% milliseconds are allowed before the agent executes the function and returns
%%% the result value, or the atom infinity to wait indefinitely. If no result
%%% is received within the specified time, the function call fails and the caller
%%% exits.
%%% @end
-spec update(agent(), update_fun()) -> ok.
update(Agent, Fun) when is_function(Fun, 1) ->
update(Agent, Fun, 5000).
-spec update(agent(), update_fun(), timeout_ms()) -> ok.
update(Agent, Fun, Timeout) when is_function(Fun, 1) ->
gen_server:call(Agent, {update, Fun}, Timeout).
%%% @doc
%%% Updates the agent state via the given function.
%%%
%%% Same as update/3 but a module, function, and arguments are expected
%%% instead of an anonymous function. The state is added as first
%%% argument to the given list of arguments.
%%% @end
-spec update(agent(), module(), atom(), [term()], timeout_ms()) -> ok.
update(Agent, Module, Fun, Args, Timeout) ->
gen_server:call(Agent, {update, {Module, Fun, Args}}, Timeout).
-spec update(agent(), module(), atom(), [term()], timeout_ms(), timeout_ms()) -> ok.
update(Agent, Module, Fun, Args, Timeout, _GenTimeout) ->
gen_server:call(Agent, {update, {Module, Fun, Args}}, Timeout).
%%% @doc
%%% Performs a cast (fire and forget) operation on the agent state.
%%%
%%% The function Fun is sent to the Agent which invokes the function
%%% passing the agent state. The return value of Fun becomes the new
%%% state of the agent.
%%%
%%% Note that cast returns ok immediately, regardless of whether Agent (or
%%% the node it should live on) exists.
%%% @end
-spec cast(agent(), update_fun()) -> ok.
cast(Agent, Fun) when is_function(Fun, 1) ->
gen_server:cast(Agent, {cast, Fun}).
%%% @doc
%%% Performs a cast (fire and forget) operation on the agent state.
%%%
%%% Same as cast/2 but a module, function, and arguments are expected
%%% instead of an anonymous function. The state is added as first
%%% argument to the given list of arguments.
%%% @end
-spec cast(agent(), module(), atom(), [term()]) -> ok.
cast(Agent, Module, Fun, Args) ->
gen_server:cast(Agent, {cast, {Module, Fun, Args}}).
%%% @doc
%%% Synchronously stops the agent with the given Reason.
%%%
%%% It returns ok if the agent terminates with the given
%%% reason. If the agent terminates with another reason, the call will
%%% exit.
%%%
%%% This function keeps OTP semantics regarding error reporting.
%%% If the reason is any other than normal, shutdown or
%%% {shutdown, _}, an error report will be logged.
%%% @end
-spec stop(agent()) -> ok.
stop(Agent) ->
stop(Agent, normal, infinity).
-spec stop(agent(), term()) -> ok.
stop(Agent, Reason) ->
stop(Agent, Reason, infinity).
-spec stop(agent(), term(), timeout_ms()) -> ok.
stop(Agent, Reason, Timeout) ->
gen_server:stop(Agent, Reason, Timeout).
%%%=============================================================================
%%% gen_server callbacks
%%%=============================================================================
%%% @private
init(Fun) when is_function(Fun, 0) ->
_ = initial_call(Fun),
{ok, run(Fun, [])};
init({Module, Fun, Args}) ->
_ = initial_call({Module, Fun, Args}),
{ok, run({Module, Fun, Args}, [])}.
%%% @private
handle_call({get, Fun}, _From, State) ->
{reply, run(Fun, [State]), State};
handle_call({get_and_update, Fun}, _From, State) ->
case run(Fun, [State]) of
{Reply, NewState} ->
{reply, Reply, NewState};
Other ->
{reply, {error, {bad_return_value, Other}}, State}
end;
handle_call({update, Fun}, _From, State) ->
{reply, ok, run(Fun, [State])}.
%%% @private
handle_cast({cast, Fun}, State) ->
{noreply, run(Fun, [State])}.
%%% @private
handle_info(_Info, State) ->
{noreply, State}.
%%% @private
terminate(_Reason, _State) ->
ok.
%%% @private
code_change(_OldVsn, State, Fun) ->
{ok, run(Fun, [State])}.
%%%=============================================================================
%%% Internal functions
%%%=============================================================================
%% @private
initial_call(MFA) ->
_ = put('$initial_call', get_initial_call(MFA)),
ok.
%% @private
get_initial_call(Fun) when is_function(Fun, 0) ->
{module, Module} = erlang:fun_info(Fun, module),
{name, Name} = erlang:fun_info(Fun, name),
{Module, Name, 0};
get_initial_call({Module, Fun, Args}) ->
{Module, Fun, length(Args)}.
%% @private
run({Module, Fun, Args}, Extra) ->
apply(Module, Fun, Extra ++ Args);
run(Fun, Extra) ->
apply(Fun, Extra).