ExActor v2.2.4 ExActor.Operations
Macros that can be used for simpler definition of GenServer
operations
such as casts or calls.
For example:
defcall request(x, y), state: state do
set_and_reply(state + x + y, :ok)
end
will generate two functions:
def request(server, x, y) do
GenServer.call(server, {:request, x, y})
end
def handle_call({:request, x, y}, _, state) do
{:reply, :ok, state + x + y}
end
There are various helper macros available for specifying responses. For more details
see ExActor.Responders
.
Request format (passed to handle_call/3
and handle_cast/2
)
- no arguments ->
:my_request
- one arguments ->
{:my_request, x}
- more arguments ->
{:my_request, x, y, ...}
Common options
:when
- specifies guards (see Pattern matching below for details):export
- applicable indefcall/3
anddefcast/3
. If provided, specifies the server alias. In this case, interface functions will not accept the server as the first argument, and will insted use the provided alias. The alias can be an atom (for locally registered processes),{:global, global_alias}
or a via tuple ({:via, registration_module, alias}
).
Pattern matching
defcall a(1), do: ...
defcall a(x), when: x > 1, do: ...
defcall a(x), when: [interface: x > 1, handler: x < state], do: ...
defcall a(x), state: 1, do: ...
defcall a(_), state: state, do: ...
Details
defcall
and other similar constructs usually define a clause for two
functions: the interface function and the handler function. If you’re writing
multi-clauses, the following rules apply:
- Arguments are pattern-matched in the interface and in the handler function.
- The
:state
pattern is used in the handler function. - The
:when
option by default applies to both, the interface and the handler function. You can however specify separate guards withwhen: [interface: ..., handler: ...]
. It’s not necessary to provide both options towhen
.
ExActor
will try to be smart to some extent, and defer from generating the
interface clause if it’s not needed.
For example:
defcall foo(_, _), state: nil, do: ...
defcall foo(x, y), state: state, do: ...
will generate only a single interface function that always matches its arguments
and sends them to the server process. There will be of course two handle_call
clauses.
The same holds for more elaborate pattern-matches:
defcall foo(1, 2), ...
defcall foo(x, y), when: x > y, ...
defcall foo(_, _), state: nil, do: ...
defcall foo(x, y), state: state, do: ...
The example above will generate three interface clauses:
def foo(1, 2)
def foo(x, y) when x > y
def foo(x, y)
Of course, there will be four handle_call
clauses, each with the corresponding
body provided via do
option.
Separating interface and handler clauses
If you want to be more explicit about pattern matching, you can use a body-less construct:
defcall foo(x, y)
This will generate only the interface clause that issues a call (or a cast in
the case of defcast
) to the server process.
You can freely use multiple defcall
body-less clauses if you need to pattern
match arguments.
To generate handler clauses you can use defhandlecall/3
:
defhandlecall foo(_, _), state: nil, do: ...
defhandlecall foo(x, y), state: state, do: ...
This approach requires some more typing, but it’s more explicit. If you need to perform a complex combination of pattern matches on arguments and the state, it’s probably better to use this technique as it gives you more control over what is matched at which point.
Summary
Macros
Defines an abcast operation
Same as defabcast/3
but the interface function is private
Defines the call callback clause and the corresponding interface fun
Same as defcall/3
but the interface function is private
Defines the cast callback clause and the corresponding interface fun
Same as defcast/3
but the interface function is private
Similar to defcall/3
, but generates just the handle_call
clause,
without creating the interface function
Similar to defcast/3
, but generates just the handle_call
clause,
without creating the interface function
Defines the info callback clause. Responses work just like with casts
Similar to defstart/3
but generates just the init
clause
Defines a multicall operation
Same as defmulticall/3
but the interface function is private
Defines the starter function and initializer body
Same as defstart/2
but the interface function is private
Macros
Defines an abcast operation.
defabcast my_request(x, y), do: ...
...
# If the process is locally registered via `:export` option
MyServer.my_request(2, 3)
MyServer.my_request(nodes, 2, 3)
# The process is not locally registered via `:export` option
MyServer.my_request(:local_alias, 2, 3)
MyServer.my_request(nodes, :local_alias, 2, 3)
Same as defabcast/3
but the interface function is private.
Defines the call callback clause and the corresponding interface fun.
Call-specific options:
:timeout
- specifies the timeout used inGenServer.call
(see below for details):from
- matches the caller inhandle_call
.
Timeout
defcall long_call, state: state, timeout: :timer.seconds(10), do: ...
You can also make the timeout parameterizable
defcall long_call(...), timeout: some_variable, do: ...
This will generate the interface function as:
def long_call(..., some_variable)
where some_variable
will be used as the timeout in GenServer.call
. You
won’t have the access to this variable in your body though, since the body
specifies the handler function. Default timeout value can also be provided via
standard \\
syntax.
Same as defcall/3
but the interface function is private.
Can be useful when you need to do pre/post processing in the caller process.
def exported_interface(...) do
# do some client side preprocessing here
my_request(...)
# do some client side post processing here
end
# Not available outside of this module
defcallp my_request(...), do: ...
Defines the cast callback clause and the corresponding interface fun.
Same as defcast/3
but the interface function is private.
Can be useful when you need to do pre/post processing in the caller process.
def exported_interface(...) do
# do some client side preprocessing here
my_request(...)
# do some client side post processing here
end
# Not available outside of this module
defcastp my_request(...), do: ...
Similar to defcall/3
, but generates just the handle_call
clause,
without creating the interface function.
Similar to defcast/3
, but generates just the handle_call
clause,
without creating the interface function.
Defines the info callback clause. Responses work just like with casts.
defhandleinfo :some_message, do: ...
defhandleinfo :another_message, state: ..., do:
Similar to defstart/3
but generates just the init
clause.
Note: keep in mind that defstart
wraps arguments in a tuple. If you want to
handle defstart start(x)
, you need to define definit {x}
Defines a multicall operation.
defmulticall my_request(x, y), do: ...
...
# If the process is locally registered via `:export` option
MyServer.my_request(2, 3)
MyServer.my_request(nodes, 2, 3)
# The process is not locally registered via `:export` option
MyServer.my_request(:local_alias, 2, 3)
MyServer.my_request(nodes, :local_alias, 2, 3)
Request format is the same as in defcall/3
. Timeout option works just like
with defcall/3
.
Same as defmulticall/3
but the interface function is private.
Defines the starter function and initializer body.
# defines and export start/2
defstart start(x, y) do
# runs in init/1 callback
initial_state(x + y)
end
# defines and export start_link/2
defstart start_link(x, y) do
# runs in init/1 callback
initial_state(x + y)
end
You can also provide additional GenServer
options via :gen_server_opts
option.
defstart start(x, y), gen_server_opts: [spawn_opts: [min_heap_size: 10000]], do: ...
If you need to set GenServer
options at runtime, use gen_server_opts: :runtime
and
then the starter function will receive one more argument where you can pass options:
defstart start(x, y), gen_server_opts: :runtime do
...
end
...
MyServer.start(x, y, name: :foo, spawn_opts: [min_heap_size: 10000])
Body can be omitted. In this case, just the interface function is generated.
This can be useful if you want to define both start
and start_link
:
defstart start(x, y)
defstart start_link(x, y) do
# runs for both cases
end
Keep in mind that generated init/1
matches on the number of arguments, so this won’t work:
defstart start_link(x)
defstart start_link(x, y) do
# doesn't handle start_link(x)
end
If you want to handle various versions, you can just define start heads without the body,
and then use definit/2
or just implement init/1
.
Other notes
- If the
export
option is set while usingExActor
, it will be used in starters, and the server process will be registered under a given alias. - For each specified clause, there will be one corresponding interface function clause.
Request format (arg passed to init/1
)
- no arguments ->
nil
- one arguments ->
{x}
- more arguments ->
{x, y, ...}
Same as defstart/2
but the interface function is private.
Can be useful when you need to do pre/post processing in the caller process.
defmodule MyServer do
def start_link(x, y) do
...
do_start_link(x, y)
...
end
defstartp do_start_link(x, y), link: true do
...
end
end