ExActor.Operations
Macros that can be used for simpler definition of GenServer
operations such as casts or calls.
Summary↑
defabcast(req_def, options \\ [], body \\ []) | Defines an abcast operation |
defabcastp(req_def, options \\ [], body \\ []) | Same as |
defcall(req_def, options \\ [], body \\ []) | Defines the call callback clause and the corresponding interface fun |
defcallp(req_def, options \\ [], body \\ []) | Same as |
defcast(req_def, options \\ [], body \\ []) | Defines the cast callback clause and the corresponding interface fun. See |
defcastp(req_def, options \\ [], body \\ []) | Same as |
defhandlecall(req_def, options \\ [], body \\ []) | Similar to |
defhandlecast(req_def, options \\ [], body \\ []) | Similar to |
defhandleinfo(msg, opts \\ [], body) | Defines the info callback clause. Responses work just like with casts |
definit(arg \\ {:_, [], ExActor.Operations}, opts) | Similar to |
defmulticall(req_def, options \\ [], body \\ []) | Defines a multicall operation |
defmulticallp(req_def, options \\ [], body \\ []) | Same as |
defstart(arg1, opts \\ [], body \\ []) | Defines the starter function and initializer body |
defstartp(arg1, options \\ [], body \\ []) | Same as |
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)
Request format is the same as in defcall/3
Same as defabcast/3
but the interface function is private.
Defines the call callback clause and the corresponding interface fun.
defcall operation, do: reply(response)
defcall get, state: state, do: reply(state)
defcall inc(x \\ 1), state: state, do: set_and_reply(state + x, response)
Request format (passed to handle_call/3
)
- no arguments ->
:my_request
- one arguments ->
{:my_request, x}
- more arguments ->
{:my_request, x, y, ...}
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.
Pattern matching
defcall a(1), do: ...
defcall a(2), do: ...
defcall a(x), when: x > 1, do: ...
defcall a(x), state: 1, do: ...
defcall a(_), state: state, do: ...
Details
A single defcall
(or similar) construct usually defines a clause for two functions: the interface function and the handler function. For example:
defcall foo(x, y), state: state, do: ...
will generate:
def foo(pid, x, y), do: ...
def handle_call({:foo, x, y}, _, state), do: ...
If you're writing multi-clauses, the following rules apply:
- Arguments are pattern-matched in interface and in handler function.
- The
when:
clause applies only to the interface function. - The
state
argument is pattern-matched in the handler function.
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.
You can freely mix this with argument 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)
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 complex combinations 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.
Also, this approach allows you to use the state in the when
option (guard):
defcall a(x)
defhandlecall a(x), state: state, when: state > 0, do: ...
defhandlecall a(x), state: state, do: ...
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. See defcall/3
for more details.
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 info/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 defhandleinfo/2
or just implement handle_info/1
.
Other notes
- If the
export
option is set while usingExActor
, it will be honored in starters. - You can use patterns in arguments. Pattern matching is performed in the interface function. For each specified clause, there will be one corresponding interface function clause.
- You can provide additional guard via
:when
option. The guard applies to theinit/1
.
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