In 2011, when I was new to Erlang and functional programming, the biggest question I had was something like this:
When there is no state in functional programming, how do you store, retrieve and update state?”
I read about gen_server that really just was the point where I realised that although everyone kept saying “There is no state in functional programming” you actually do have mechanisms to store state in Erlang/Elixir for later use.
So I dived into it and learned how it’s done.
How it’s done
Actually the concept behind it is not so much rocket-science. Basically it consists of a function calling itself recursively and communicating with other processes which want to store and update the state. If the state needs to be changed, the next call will be made with the new state.
Let’s build a Stack module that holds it’s values and allows users to push
and pop
values from it.
We start off with a simple function that will run and keep the state by recursively calling itself and receiving basic messages to handle changes to the state:
defmodule Stack do
def loop(state) do
receive do
{_from, :push, value} ->
loop([value | state])
{from, :pop} ->
[h | t] = state
send(from, {:reply, h})
loop(t)
end
loop(state)
end
end
loop/1
waits for new messages to arrive and uses receive
to block until there are messages for this process which will be pattern-machted against. As you can see, there are two patterns for the two operations we support currently.
The first one will match {_from, :push, value}
which handles pushes on to the stack. The sending pid _from
is not needed as this is an asynchronous operation. It starts the next iteration with the new value
added as the new first element of state
which will again wait for new messages to arrive.
The other pattern {from, :pop}
handles the pop operation. The message sender from
will receive back a message with the first element of state
extracted while the rest of the list is given to yet another call to loop/1
which will then wait for another message to arrive.
Test it
Let’s take our stack implementation for a spin. Start iex
, paste the above module into it and we are ready to go! To start the stack, just spawn the function as a process:
iex> pid = spawn(Stack, :loop, [[]])
#PID<0.108.0>
spawn/3
expects a module (Stack), the function name as an atom (:loop) and a list of arguments, which in our case is an empty list. This makes the initial call, which is essentially Stack.loop([])
. The returned pid
is a process identifier to which all messages need to be sent.
To push a new value on to the stack, we need to send that pid
a message like this:
iex> send(pid, {self(), :push, 1})
{#PID<0.88.0>, :push, 1}
self()
returns the value of the current process (which is iex’s pid
in our case) and send will return the message sent. For this operation we don’t need it, but it’s a good practice to send it anyway. Also we don’t see any change to the stack yet, but remember: the push operation is async.
Now let’s see the change in the stack by popping a value from the stack:
iex> send(pid, {self(), :pop})
{#PID<0.88.0>, :push, 1}
Here, self()
is required, because we expect a message back to our iex-pid containing the value. Again, send
returns the message sent and nothing else happens. That’s because we didn’t read the message we got back yet. Let’s use the iex helper flush
to retrieve the contents of our main process’ mailbox:
iex> flush
{:reply, 1}
:ok
And there it is; as expected, the stack contained the value 1
.
a clean client interface
As our stack is now working, we should build a nice, clean API around it. A user that would want to use the stack module should not care about which messages are required to sent to the process. Instead he/she should use only functions to place values on the stack:
Let’s extend the module and add a bunch of short functions that build up the API and a clean client user interface.
defmodule Stack do
...
def start_link(state) do
spawn_link( __MODULE__ , :loop, [init(state)])
end
def init(state), do: state
def push(pid, value) do
send(pid, {self(), :push, value})
:ok
end
def pop(pid) do
send(pid, {self(), :pop})
receive do
{:reply, value} -> value
end
end
end
If you compare this against the callbacks that you would need to implement when using GenServer, then you will see that it uses the same naming and function signatures. This is not by accident - it is to show you that GenServer does exactly the same as the above code. Except for a little more error handling and in a more generic way of course. But the concepts remain the same.
Btw. thanks to tail call optimization the call stack of the loop/1
function is constant in terms of memory usage and therefor not a problem when running forever and ever.
Here I will leave you off to some experimenting with state as messages and recursive functions. Happy Elixir’ing!
Top comments (0)