ExActor
Simplified implementation and usage of gen_server
based actors in Elixir. This library is inspired by (though not depending on) GenX, but in addition, removes some more boilerplate, and changes some semantics of the handle_call/cast responses.
If you're new to Erlang, and are not familiar on how gen_server works, I strongly suggest you learn about it first. It's really not that hard, and you can use Elixir docs as the starting point. Once you're familiar with gen_server, you can use ExActor to make your actors (gen_servers) more compact.
Status: I use it in production.
Online documentation is available here.
The stable package is also available on hex.
Basic usage
Be sure to include a dependency in your mix.exs
:
deps: [{:exactor, "~> 1.0.0"}, ...]
defmodule Actor do
use ExActor.GenServer
definit do: initial_state(some_state)
defcast inc(x), state: state, do: new_state(state + x)
defcall get, state: state, do: reply(state)
defcall long_call, state: state, timeout: :timer.seconds(10), do: heavy_transformation(state)
# Interface functions foo and bar are private to this module. This is useful when
# you need to do pre/post processing in the client process. You can simply create a plain
# exported interface function, and then call these private functions to issue request to
# the server process.
defcastp foo, ...
defcallp bar, ...
definfo :some_message, do: ...
end
# initial state is set to start argument
{:ok, act} = Actor.start(1)
Actor.get(act) # 1
Actor.inc(act, 2)
Actor.get(act) # 3
Predefines
A predefine is an ExActor mixin that provides some default implementations for gen_server
callbacks. Following predefines are currently provided:
ExActor.GenServer
- Allgen_server
callbacks are provided by GenServer from Elixir standard library.ExActor.Strict
- Allgen_server
callbacks are provided. The default implementations for all exceptcode_change
andterminate
will cause the server to be stopped.ExActor.Tolerant
- Allgen_server
callbacks are provided. The default implementations ignore all messages without stopping the server.ExActor.Empty
- No default implementation forgen_server
callbacks are provided.
It is up to you to decide which predefine you want to use. See online docs for detailed description. You can also build your own predefine. Refer to the source code of the existing ones as a template.
Singleton actors
defmodule SingletonActor do
# The actor process will be locally registered under an alias
# provided in export option.
use ExActor.GenServer, export: :some_registered_name
# you can also use via, and global
# use ExActor.GenServer, export: {:global, :some_registered_name}
# use ExActor.GenServer, export: {:via, :gproc, :some_registered_name}
defcall get, state: state, do: reply(state)
defcast set(x), do: new_state(x)
end
SingletonActor.start
SingletonActor.set(5)
SingletonActor.get
Handling of return values
definit do: initial_state(arg) # sets initial state
definit do: {:ok, arg} # standard gen_server response
defcall a, state: state, do: reply(response) # responds 5 but doesn't change state
defcall b, do: set_and_reply(new_state, response) # responds and changes state
defcall c, do: {:reply, response, new_state} # standard gen_server response
defcast c, do: noreply # doesn't change state
defcast d, do: new_state(new_state) # sets new state
defcast f, do: {:noreply, new_state} # standard gen_server response
definfo c, do: noreply # doesn't change state
definfo d, do: new_state(new_state) # sets new state
definfo f, do: {:noreply, new_state} # standard gen_server response
Simplified starting
Actor.start # same as Actor.start(nil)
Actor.start(init_arg)
Actor.start(init_arg, options)
Actor.start_link # same as Actor.start_link(nil)
Actor.start_link(init_arg)
Actor.start_link(init_arg, options)
Dynamic registration
Actor.start(init_arg, name: :some_registered_name, ...) # registers locally
Actor.start(init_arg, name: {:local, :some_registered_name}, ...) # registers locally
Actor.start(init_arg, name: {:global, :some_registered_name}, ...) # registers globally
Actor.start(init_arg, name: {:via, :gproc, :some_registered_name}, ...) # registers via external module
# same for start_link
Starter functions are overridable. You can optionally specify that you don't want them:
use ExActor.GenServer, starters: false
Simplified initialization
# define initial state
use ExActor.GenServer, initial_state: HashDict.new
# alternatively as the function
definit do: HashSet.new
# using the input argument
definit x do
x + 1
|> initial_state
end
Handling messages
definfo :some_message, do:
definfo :another_message, state: ..., do:
Pattern matching
defcall a(1), do: ...
defcall a(2), do: ...
defcall a(x), state: 1, do: ...
defcall a(x), when: x > 1, do: ...
defcall a(x), state: state, when: state > 1, do: ...
defcall a(_), do: ...
definit :something, do: ...
definit x, when: ..., do: ...
definfo :msg, state: {...}, when: ..., do: ...
Note: all call/cast matches take place at the handle_call
or handle_cast
level. The interface function simply passes the arguments to appropriate gen_server
function. Consequently, if a match fails, the server will crash.
Skipping interface funs
# interface fun will not be generated, just handle_call clause
defcall unexported, export: false, do: :unexported
Using from
defcall a(...), from: {from_pid, ref} do
...
end
Runtime friendliness
May be useful if calls/casts simply delegate to some module/functions.
defmodule DynActor do
use ExActor.GenServer
for op <- [:op1, :op2] do
defcall unquote(op), state: state do
SomeModule.unquote(op)(state)
end
end
end
Simplified data abstraction delegation
Macro delegate_to
is provided to shorten the definition when the state is implemented as a functional data abstraction, and operations simply delegate to that module. Here's an example:
defmodule HashDictActor do
use ExActor.GenServer
import ExActor.Delegator
delegate_to HashDict do
init
query get/2
trans put/3
end
end
This is equivalent of:
defmodule HashDictActor do
use ExActor.GenServer
definit do: HashDict.new
defcall get(k), state: state do
HashDict.get(state, k)
end
defcast put(k, v), state:state do
HashDict.put(state, k, v)
|> new_state
end
end
You can freely mix delegate_to
with other macros, such as defcall
, defcast
, and others.