From Stateful Process to GenServer in Elixir

When we talk about Functional Languages like Elixir we’re talking about our functions being pure where the same input gives you the same output. We’re talking about not depending on keeping state receiving different mutations. So if you need to keep the state in Elixir, how can it be done? Well, GenServer does, but we’ll get there.

Goal

We’ll create a stateful process and refactor it until gets the GenServer implementation, in this way you can learn how the state works and how GenServer encapsulates it.

Loop

The magic behind keeping state is to keep the code in a loop, so you can change a provided value and call itself again and again and again and…

defmodule GenericServer do
  def loop(state) do
    IO.puts("Listening with state: #{inspect(state)}")

    receive do
      {:call, caller, message} ->
        IO.puts("#{inspect(caller)} sent #{inspect(message)}")

        new_state = Map.put(state, :total, state[:total] + 1)

      loop(new_state)
    end
  end
end

This function will loop the process and wait for some message that can be matched with {:call, caller, message}. This match will increase the total key and this new map will be given to the loop again. If you do not call the loop again, the process will die.

Since receive blocks the process we’ll start it in a new process different from the terminal using spawn and initialize the state:

pid = spawn(GenericServer, :loop, [%{total: 0}])
#PID<0.144.0>

With PID 0.144.0 in hand we can send a message to this process:

send(pid, {:call, self(), "Will match :call"})
# #PID<0.141.0> sent "Will match :call"
# Listening with state: %{total: 1}

We sent a message to process PID matching the block :call providing the terminal PID self() as 0.141.0 and a message.

Let’s improve it and create a method to increment, decrement, and display the result:

defmodule GenericServer do
  def loop(state) do
    IO.puts("Listening with state: #{inspect(state)}")

    receive do
      {:decrement, value} ->
        new_state = Map.put(state, :total, state[:total] - value)

        loop(new_state)

      {:increment, value} ->
        new_state = Map.put(state, :total, state[:total] + value)

        loop(new_state)

      {:result, caller} ->
        new_state = state

        send(caller, new_state)

        loop(new_state)
    end
  end
end

Only the :result block receives the caller’s PID to have the opportunity to send the message back to the caller.

pid = spawn(GenericServer, :loop, [%{total: 0}])
#PID<0.144.0>

send(pid, {:increment, 7})
# Listening with state: %{total: 7}

send(pid, {:decrement, 2})
# Listening with state: %{total: 5}

send(pid, {:result, self()})
# Listening with state: %{total: 5}

Process.info(self(), :messages)
# {:messages, [%{total: 5}]}

flush
# {:result, %{total: 5}}
# :ok

Since we sent a message to the terminal, we can get the message in the mailbox. In the final we flush it to clear the messages.

To avoid mixing up the logic of the calculation with the logic to receive the messages, let’s refactor and separate the calculation into two different handles: call that returns a response to the caller and cast that doesn’t:

defmodule GenericServer do
  def loop(module, state) do
    IO.puts("Listening with state: #{inspect(state)}")

    receive do
      {:call, message, caller} ->
        new_state = module.handle_call(message, state)

        send(caller, new_state)

        loop(module, new_state)

      {:cast, message} ->
        new_state = module.handle_cast(message, state)

        loop(module, new_state)
    end
  end

  def handle_call(:result, state) do
    state
  end

  def handle_cast({:decrement, value}, state) do
    Map.put(state, :total, state[:total] - value)
  end

  def handle_cast({:increment, value}, state) do
    Map.put(state, :total, state[:total] + value)
  end
end

The receive now listen to call and cast where cast won’t send a response back. We added the module variable to identify the module that has the handles, in this case the main module itself. All handles return the state, isolating this state manipulation logic.

pid = spawn(GenericServer, :loop, [GenericServer, %{total: 0}])
# #PID<0.143.0>
# Listening with state: %{total: 0}

send(pid, {:cast, {:increment, 7}})
# Listening with state: %{total: 7}

send(pid, {:cast, {:decrement, 2}})
# Listening with state: %{total: 5}

send(pid, {:call, :result, self()})
# Listening with state: %{total: 5}

Process.info(self(), :messages)
# {:messages, [%{total: 5}]}

It’s not easy to remember the send syntax since we need to know the order of the parameters, so let’s encapsulate the send calls:

defmodule GenericServer do
  def start(state \\ %{total: 0}) do
    spawn(__MODULE__, :loop, [__MODULE__, state])
  end

  def decrement(pid, value) do
    cast(pid, {:decrement, value})
  end

  def increment(pid, value) do
    cast(pid, {:increment, value})
  end

  def result(pid) do
    call(pid, :result, self())
  end

  def loop(module, state) do
    IO.puts("Listening with state: #{inspect(state)}")

    receive do
      {:call, message, caller} ->
        new_state = module.handle_call(message, state)

        send(caller, new_state)

        loop(module, new_state)

      {:cast, message} ->
        new_state = module.handle_cast(message, state)

        loop(module, new_state)
    end
  end

  def call(pid, message, caller) do
    send(pid, {:call, message, caller})

    receive do
      response -> response
    end
  end

  def cast(pid, message) do
    send(pid, {:cast, message})
  end

  def handle_call(:result, state) do
    state
  end

  def handle_cast({:decrement, value}, state) do
    Map.put(state, :total, state[:total] - value)
  end

  def handle_cast({:increment, value}, state) do
    Map.put(state, :total, state[:total] + value)
  end
end

Now we extract the method to execute call and cast, so the method increment, decrement, and result can call them. Since now the module is the one that calls the result method, the self() PID will be the module’s context, so to receive the result with no need to check the mailbox, we can add a receive and listen to this result.

Pay attention that we have two processes now, the PID for the spawned module and the PID of the module that asks for the result. To communicate with loop method, we send a message to the spawned module that returns it in the start method:

pid = GenericServer.start()
# Listening with state: %{total: 0}
# #PID<0.155.0>

GenericServer.increment(pid, 7)
# Listening with state: %{total: 7}

GenericServer.decrement(pid, 2)
# Listening with state: %{total: 5}

GenericServer.result(pid)
# Listening with state: %{total: 5}

Ok, keeping the PID and passing it through methods is not cool, but necessary to keep the spawned process on track. We have a trick where we can give a name for the process so we can refer to it using just the name:

defmodule GenericServer do
  @name __MODULE__

  def start(state \\ %{total: 0}) do
    pid = spawn(__MODULE__, :loop, [__MODULE__, state])

    Process.register(pid, @name)

    pid
  end

  def decrement(value) do
    cast(@name, {:decrement, value})
  end

  def increment(value) do
    cast(@name, {:increment, value})
  end

  def result() do
    call(@name, :result, self())
  end

  def loop(module, state) do
    IO.puts("Listening with state: #{inspect(state)}")

    receive do
      {:call, message, caller} ->
        new_state = module.handle_call(message, state)

        send(caller, new_state)

        loop(module, new_state)

      {:cast, message} ->
        new_state = module.handle_cast(message, state)

        loop(module, new_state)
    end
  end

  def call(pid, message, caller) do
    send(pid, {:call, message, caller})

    receive do
      response -> response
    end
  end

  def cast(pid, message) do
    send(pid, {:cast, message})
  end

  def handle_call(:result, state) do
    state
  end

  def handle_cast({:decrement, value}, state) do
    Map.put(state, :total, state[:total] - value)
  end

  def handle_cast({:increment, value}, state) do
    Map.put(state, :total, state[:total] + value)
  end
end

Now in the start method, we register the PID as the name of the module, in this way, we don’t need to transport it, just refer to it globally to call call and cast:

pid = GenericServer.start()
# Listening with state: %{total: 0}

GenericServer.increment(7)
# Listening with state: %{total: 7}

GenericServer.decrement(2)
# Listening with state: %{total: 5}

GenericServer.result()
# Listening with state: %{total: 5}

And the last refactor is separate all server logic:

defmodule GenericServer do
  def start(module, state, options) do
    pid = spawn(__MODULE__, :loop, [module, state])

    Process.register(pid, options[:name])
  end

  def loop(module, state) do
    IO.puts("Listening with state: #{inspect(state)}")

    receive do
      {:call, message, caller} ->
        new_state = module.handle_call(message, state)

        send(caller, new_state)

        loop(module, new_state)

      {:cast, message} ->
        new_state = module.handle_cast(message, state)

        loop(module, new_state)
    end
  end

  def call(pid, message, caller) do
    send(pid, {:call, message, caller})

    receive do
      response -> response
    end
  end

  def cast(pid, message) do
    send(pid, {:cast, message})
  end
end

From the client logic:

defmodule Counter do
  @name __MODULE__

  def start(state \\ %{total: 0}) do
    GenericServer.start(__MODULE__, state, name: @name)
  end

  def decrement(value) do
    GenericServer.cast(@name, {:decrement, value})
  end

  def increment(value) do
    GenericServer.cast(@name, {:increment, value})
  end

  def result() do
    GenericServer.call(@name, :result, self())
  end

  # callbacks

  def handle_call(:result, state) do
    state
  end

  def handle_cast({:decrement, value}, state) do
    Map.put(state, :total, state[:total] - value)
  end

  def handle_cast({:increment, value}, state) do
    Map.put(state, :total, state[:total] + value)
  end
end
pid = Counter.start()
# #PID<0.159.0>
# Listening with state: %{total: 0}

Counter.increment(7)
# Listening with state: %{total: 7}

Counter.decrement(2)
# Listening with state: %{total: 5}

Counter.result()
# Listening with state: %{total: 5}

Done! We could create a stateful process from scratch and understand how to send and receive messages between the process. Ok, but what is the relation of it to GenServer? Well, you just wrote a GenServer with a slight difference in the interface.

For our call callbacks we need to add one extra parameter called from in the second position. It’ll contain the PID from the caller and the unique identification of the request, but we won’t use it. All callbacks must return a tuple over a single value. Since the call method needs to return a response, the first value of the tuple is reply, the second is the value we want to reply to the caller and the third is the state:

  def handle_call(:result, _from, state) do
    {:reply, state, state}
  end

For cast callback we don’t have the from parameter, since we don’t reply to the caller, so the first key of the tuple is noreply and the second is the state:

  def handle_cast({:decrement, value}, state) do
    {:noreply, Map.put(state, :total, state[:total] - value)}
  end

  def handle_cast({:increment, value}, state) do
    {:noreply, Map.put(state, :total, state[:total] + value)}
  end

GenServer has a couple of callbacks and we implemented two of them, but don’t worry we already have generic callbacks implemented, we just need to use the module GenServer use GenServer:

defmodule Counter do
  use GenServer

  @name __MODULE__

  def start(state \\ %{total: 0}) do
    GenServer.start(__MODULE__, state, name: @name)
  end

  def decrement(value) do
    GenServer.cast(@name, {:decrement, value})
  end

  def increment(value) do
    GenServer.cast(@name, {:increment, value})
  end

  def result() do
    GenServer.call(@name, :result)
  end

  # callback

  def handle_call(:result, _from, state) do
    {:reply, state[:total], state}
  end

  def handle_cast({:decrement, value}, state) do
    {:noreply, Map.put(state, :total, state[:total] - value)}
  end

  def handle_cast({:increment, value}, state) do
    {:noreply, Map.put(state, :total, state[:total] + value)}
  end
end

Let’s test our GenServer, but now the return of the start will be a tuple too:

{:ok, pid} = Counter.start()
# {:ok, #PID<0.159.0>}

Counter.increment(7)
# :ok

Counter.decrement(2)
# :ok

Counter.result()
# %{total: 5}

Conclusion

Congratulations! You now understand how a GenServer works under the roof. If you need to manipulate the initial state you can use the callback init. If you need to split the callback in two peace, you can add an extra {:continue, value} on the return of the tuple. If you want to hold a generic message use handle_info, manipulate something before a normal exit, terminate. To handle a change module version, code_change. To intercept the state result, format_status.

You can find everything about GenServer in the documentation.

Any suggestion? Please, send me an email here.