Marmelab Blog

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.