Elixir GenServer Explained to Redux Developers
Although very young, the Elixir language has gained a lot of popularity since its creation. Whereas based upon an ancestral language / platform called Erlang BEAM, Elixir provides strong functional, concurrent and distributed capabilities. Its feel modern and fun. That's why I just could not resist to play with it.
In this post, I'll give the keys that helped me understand GenServer, the cornerstone of a lot of Elixir distributed applications / systems.
The GenServer Swiss Knife
Like any other language built on top of the Erlang VM, Elixir relies on lightweight and isolated threads (usually called processes), and messages passed between these threads. The key benefits of this mechanism are primarily fault-tolerance, distributivity and scalability.
The gen_server (for "Generic Server") is a standard module included in the underlying Erlang OTP (Open Telecommunications Platform). It offers abstractions to build a basic client <-> server system with a common shared resource (state). It was used in a lot of Open Source projects in the Elixir community.
GenServer usually acts as a central registry with its own state, on which we can perform (a)synchronous actions via message passing. In the web dev world, it can be roughly compared to an asynchronous Redux store.
So how does it compare with Redux? In the example below, I'll create a simple registry of quotes. I'll use both GenServer and Redux to show what they have in common.
The Elixir Version
Here is how to implement a registry of quotes with GenServer:
# quotes_list.ex
defmodule QuotesList do
use GenServer
# Client API
def start_link do
GenServer.start_link(__MODULE__, :ok, [])
end
def read(pid) do
GenServer.call(pid, {:read})
end
def add(pid, quote) do
GenServer.cast(pid, {:add, quote})
end
# Server Callbacks
def init(:ok) do
{:ok, []}
end
def handle_call({:read}, from, list) do
{:reply, list, list}
end
def handle_cast({:add, quote}, list) do
{:noreply, list ++ [quote]}
end
end
In this snippet, I define an Elixir module called QuotesList
. Since GenServer
comes from an underlying Erlang library, it was provided through an Elixir Bridge with a kind of "mixin" (thanks to the "use" macro).
In the Elixir world, start_link
is the common method name for starting a process which is attached to the current supervision tree. The third parameter of this method represents the initial state (an empty array) of our QuotesList module.
The Client API section can be defined as the public interface of our module. We can call read
, add
, ... from the outside to manipulate or access our store at our convenience.
On the other side, the Server Callbacks section is responsible for the inner working state manipulation of the GenServer. It responds respectively to the different calls / casts made in the Client API section, thanks to the strong pattern matching capabilities of Elixir. In the next chapter, I'll cover this section in a deeper way.
To conclude this part about the Elixir implementation, here is an example of GenServer usage for our QuotesList
module.
# another_module.ex
# Start the quote server
{:ok, pid} = QuotesList.start_link
# Adding some items
QuotesList.add(pid, "All that glitters is not gold.")
QuotesList.add(pid, "Elementary, my dear Watson.")
QuotesList.add(pid, "Houston, we have a problem.")
# Read the quotes back
QuotesList.read(pid)
The Redux Version
On the Redux side, here is the equivalent of the preceding example in ES6.
// quoteListStore.js
import { createStore } from "redux";
function quoteListReducer(state = [], action) {
switch (action.type) {
case "add":
return [...state, action.quote];
default:
return state;
}
}
return createStore(quoteListReducer);
Contrary to the Elixir implementation, there's no need to create a "read" method in Redux. We can access the state value directly with getState on the store.
Furthermore, the initial state is defined directly into the default value of the reducer and not in the createStore initializer.
Elixir relies on atom tags to bring matching capabilities to our handlers. In EcmasScript, we use a type
attribute to match the right case of the reducer switch.
In Elixir, it's necessary to access data by message passing from another process. It's not required in the EcmaScript EventLoop which is mono-threaded.
And here is how to use the Redux registry in ES:
// index.js
import quoteListStore from "./quoteListStore.js";
const addQuote = item => ({ type: "add", quote });
quoteListStore.dispatch(addQuote("All that glitters is not gold."));
quoteListStore.dispatch(addQuote("Elementary, my dear Watson."));
quoteListStore.dispatch(addQuote("Houston, we have a problem."));
quoteListStore.getState();
Call, Cast & Info Callbacks
If you've never encountered GenServer before, you've certainly asked yourself about the meaning of cast
and call
. These "verbs" corresponds to two different types of actions which can be triggered.
The cast
action consists of an asynchronous action on the store, without any expectation on the result. It means that the action is queued and ready to be processed by corresponding GenServer handle_cast
handler ASAP. It's similar to the famous dispatch method in Redux.
On the other side, the call
action consists of a synchronous action on the store. The calling process is paused until the action is processed by corresponding GenServer handle_call
handler. It's most often used to access the data.
Another info
action exists, but is not usually used. This action is triggered when an incoming message comes from an unknow source / process (not the Client API section). The corresponding handle_info
can be compared to some kind of "catchAll" handler (default
case in a Redux reducer switch / case
).
Conclusion
If I had to compare Redux and Elixir GenServer, I would say that:
- GenServer != Redux because of the nature of each running platform.
Erlang BEAM / Elixir is distributed, EcmaScript is not. - GenServer offers kind of RPC capabilities through calls. Redux doesn't.
- When the only purpose of Redux is to manipulate the "state", it's not the case of GenServer.
- Redux reducer logic is centralized, GenServer logic is split in multiple
handle_*
methods.
You may also be interested by the following related links to go further into Elixir and GenServer.