Erlang's gen_server is already a pretty simple way to implement a server behaviour. Elixir's GenServer.Behaviour takes that and makes it even simpler, by providing sane default implementations of the gen_server callback functions. But there's a library called ExActor that takes it one step further. Let's have a look at how it simplifies the creation of servers to its logical conclusion.

Project

Let's go ahead and start a new project: mix new exactor_test; cd exactor_test. The first thing we need to do is add ExActor to the deps, so open up mix.exs and make the deps look like the following:

  defp deps do
    [
      { :exactor, github: "sasa1977/exactor" }
    ]
  end

Then fetch it with mix deps.get.

Basic Server

Now open up the test/exactor_test_test.exs file and let's start building out a test to try this thing out.

defmodule ExactorTestTest do
  use ExUnit.Case

  test "a basic actor" do
    {:ok, list} = ListActor.start([])
    assert ListActor.get(list) == []
    :ok = ListActor.put(list, :banana)
    assert ListActor.get(list) == [:banana]
    :ok = ListActor.put(list, :apple)
    assert ListActor.get(list) == [:banana, :apple]
    :banana = ListActor.take(list, :banana)
    assert ListActor.get(list) == [:apple]
  end
end

This will serve as a decent starting point for a really basic server. Now let's open up lib/list_actor.ex and see how to implement this server using ExActor:

defmodule ListActor do
  use ExActor

  defcall get, state: state, do: state
  defcast put(x), state: state, do: new_state(state ++ [x])
  defcast take(x), state: state, do: new_state(List.delete(state, x))
end

Run the tests, and they should pass.

That's a lot shorter than it would have been using just raw GenServer.Behaviour. defcall and defcast are macros. They simplify a lot of the common boilerplate that comes up when making servers like this. You can, of course, still fall back to GenServer.Behaviour if you find that ExActor can't help you out.

That's not all that ExActor can do, so let's look at a few more use cases.

Singleton Server

Next, we'll build a singleton server. This will just be a server that only exists once, can be referenced by name, etc. So basically, a server that knows its name and registers itself on initialization. Let's add another test for it. Open back up test/exactor_test_test.exs and add the following:

  test "a singleton" do
    CountActor.start(1)
    assert CountActor.get == 1
    CountActor.inc
    assert CountActor.get == 2
    CountActor.inc
    CountActor.inc
    CountActor.inc
    assert CountActor.get == 5
    CountActor.dec
    assert CountActor.get == 4
  end

Note that we didn't track the pid of the server we started, and we didn't have to pass it in each time. Now let's implement this, in lib/count_actor.ex:

defmodule CountActor do
  use ExActor, export: :counter

  defcall get, state: state, do: state
  defcast inc, state: state, do: new_state(state + 1)
  defcast dec, state: state, do: new_state(state - 1)
end

Run the tests, and they should pass.

The only substantial difference here is that when using ExActor, we pass an export option to it naming the atom we'd like to register the server under. Once that's done, the call and cast public api functions know they don't need to have a pid passed into them, and instead they shuffle that exported atom in place of the server pid when ultimately calling on :gen_server.handle_cast and the like under the hood.

Initial State

You can also easily define an initial state. Let's modify the list to start out with an empty list as its state, and modify our test to not pass it in:

  test "a basic actor" do
    {:ok, list} = ListActor.start
    assert ListActor.get(list) == []
    :ok = ListActor.put(list, :banana)
    assert ListActor.get(list) == [:banana]
    :ok = ListActor.put(list, :apple)
    assert ListActor.get(list) == [:banana, :apple]
    :ok = ListActor.take(list, :banana)
    assert ListActor.get(list) == [:apple]
  end

Then modifying lib/list_actor.ex:

  use ExActor, initial_state: []

Pattern Matching

We'll also modify our CountActor to respond that its state is :two rather than 2 on the call to get, in the event that its state is in fact 2. This will let us have a look at pattern matching. Tweak the test:

  test "a singleton" do
    CountActor.start(1)
    assert CountActor.get == 1
    CountActor.inc
    assert CountActor.get == :two
    CountActor.inc
    CountActor.inc
    CountActor.inc
    assert CountActor.get == 5
    CountActor.dec
    assert CountActor.get == 4
  end

Now open up lib/count_actor.ex and pattern match on this case:

  defcall get, state: state, when: state == 2, do: :two
  defcall get, state: state, do: state

Run the tests, and you can see that it works as you'd expect.

Summary

That wraps up an overview of this library. There are a few more features I've not covered, but this gets at the meat of it and shows how easy it can make building a generic server. You can read more in Resources section of the episode notes. See you soon!

Resources