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 in defcall/3 and defcast/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 with when: [interface: ..., handler: ...]. It's not necessary to provide both options to when.

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

defabcast(req_def, options \\ [], body \\ [])

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)
defabcastp(req_def, options \\ [], body \\ [])

Same as defabcast/3 but the interface function is private.

defcall(req_def, options \\ [], body \\ [])

Defines the call callback clause and the corresponding interface fun.

Call-specific options:

  • :timeout - specifies the timeout used in GenServer.call (see below for details)
  • :from - matches the caller in handle_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.

defcallp(req_def, options \\ [], body \\ [])

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: ...
defcast(req_def, options \\ [], body \\ [])

Defines the cast callback clause and the corresponding interface fun.

defcastp(req_def, options \\ [], body \\ [])

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: ...
defhandlecall(req_def, options \\ [], body \\ [])

Similar to defcall/3, but generates just the handle_call clause, without creating the interface function.

defhandlecast(req_def, options \\ [], body \\ [])

Similar to defcast/3, but generates just the handle_call clause, without creating the interface function.

defhandleinfo(msg, opts \\ [], body)

Defines the info callback clause. Responses work just like with casts.

defhandleinfo :some_message, do: ...
defhandleinfo :another_message, state: ..., do:
definit(arg \\ {:_, [], ExActor.Operations}, opts)

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}

defmulticall(req_def, options \\ [], body \\ [])

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.

defmulticallp(req_def, options \\ [], body \\ [])

Same as defmulticall/3 but the interface function is private.

defstart(arg1, opts \\ [], body \\ [])

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 using ExActor, 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, ...}
defstartp(arg1, options \\ [], body \\ [])

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