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:

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.