I recently gave a talk at Empex LA in which I talked about my desire to see simplifications and enhancements to using some of the OTP behaviors offered in Elixir. In this
GenServers are processes that have
This is easy to manage if your GenServer only needs to manage a single piece of information. But as soon as you find that your GenServer needs multiple pieces of information in state, you need to substantially refactor it.
Suppose we have a
State is simple enough to work with here.
But let’s suppose we wanted to be able to increment by values other than just 1?
First we’ll make a struct for the module:
We’ll also need a type:
Now, when we start the GenServer, we want to specify that
increment_by value. For backwards compatibility, we will default to an
increment_by value of 1:
And now the
init function needs updating too:
And when we want to check the current value of the integer, we can no longer just return state; we need to specifically grab the
value part of our struct:
And, of course, when we increment, we can no longer just
state + 1
Here is our now-refactored GenServer:
To recap, we had to add
What I’d like to see
I’d like to take some inspiration for how ExUnit handles test contexts and propose a new way of managing GenServer state.
State as map
First off, instead of GenServer state being
Now your individual functions no longer need to be concerned with the overall structure of
state; you can instead pattern match on just the pieces of state that your function needs, returning a map of the values that you want to have merged into
state. This gives your GenServer state the freedom to store more than a single value without burdening all of your
handle_cast callbacks with the complexity of that.
In case you have a use case where it would be better to replace the entire state with a brand new state(which is how GenServers currently work), this could be accomplished with a different reply atom, like
It may also be worth considering adding helper functions that can construct these return tuples, saving the developer from needing to remember a specific tuple structure.
Separating config data from state data
Let’s take this a step further. In our example GenServer, we have 2 types of data in our state: static configuration data that affects the GenServer’s behavior, and state data.
If our config is not stateful, let’s not keep it in state at all! To do this, we have
handle_cast take a parameter for
state, and a separate parameter for
Now, we can treat our
config as an immutable property of the GenServer because in your message handler callbacks you aren’t expected to update your config. As an added bonus, this provides you with some safety in knowing that you config won’t accidentally get overwritten by a bad merge of data into
The config can be static in this case, but that doesn’t mean we can never update it! This GenServer can provide a built-in
update_config callback. After sending an
update_config message to the GenServer, subsequent messages would be processed with the new config.
You could have the option to implement your own version of this in case you needed to have special handling(for instance, maybe you want to write the change to a log or a monitoring service).
Putting it all together
Let’s take a look at how our theoretical GenServer might now look with these characteristics:
By having the state data always be a map, it’s never a big leap to add additional values. And if we want to change the amount we increment by, it’s nice and easy:
This would be a simple quality of life improvement to using the
In a future blog post, we will implement our own GenServer OTP behavior that follows these semantics.