One of the best resources for learning Elixir is ElixirSchool.com. Elixir School does a great job of separating its content into a Basics section and an Advanced section. However, even after becoming fairly comfortable with the Basics section, I struggled to understand GenServer (in the Advanced section). This is my attempt to explain how GenServer does what it does to others, in the hopes that it will solidify my own understanding as well as save beginners to Elixir some time. If you only care about using GenServer, Elixir’s Getting Started Guide’s GenServer section is a crisp, clear explanation.
What is GenServer
The Open Telecom Platform (OTP) is a set of libraries that ships with Erlang. One of the libraries within the OTP framework is GenServer. (We are glossing over some technical details for now, which we will come back to).
GenServer is nothing more than a module. Like any other module, it is simply a container for a set of functions and / or macros. GenServer happens to be a type of module called a behavior. A behavior is simply a way to define what functions a module must implement.
A Very Important Macro Defined in GenServer
Again, GenServer is nothing more than a container for functions and macros. (Note that, for our purposes, a macro is simply a piece of code that creates new code).
One of the most important definitions in GenServer is the __using__/1 macro. Why is this so important? Well, the way that we use GenServer is by defining our own module and, within our module, typing use GenServer. As explained by in this post, use GenServer will execute GenServer.__using__, which will inject code directly into our module.
Translation of GenServer.__using__
At this point, I hope you are convinced that GenServer is nothing more than a container with definitions for macros and functions.
One of those definitions is for the GenServer.__using__ macro, which is invoked when we write use GenServer.
The invocation of that macro injects code directly into our module.
For example, the following code:
Will “expand” into:
I know that this is what TodoList will expand to because that is the code that the GenServer.__using__ macro is injecting into that module.
The Technical Details
One of the pieces of code that immediately caught my eye when I saw the expanded TodoList module was this:
Earlier in this post I mentioned that OTP ships with a library called GenServer. Well, that was a gross oversimplification of the technical details. OTP ships with a library / module called :gen_server. It is a behavior module, which means that it defines a set of functions that must be implemented by our module — in this case, the TodoList module.
But wait a minute! Didn’t we just use GenServer? Isn’t that how 3 lines of code expand to 46? Yes, we did, but GenServer is an Elixir module. From the viewpoint of the computer, GenServer is a completely distinct module from :gen_server.
So why have both GenServer and :gen_server? Well, GenServer does two things for us.
(For those of you wondering, all module names in Elixir are converted to atoms. For example, the TodoList module is converted to :“Elixir.TodoList” and GenServer is converted to :“Elixir.GenServer”. Erlang modules are not namespaced under Elixir, which is why we refer to simply :gen_server).
First, it injects @behaviour :gen_server into our module, so that our module must implement the functions expected by :gen_server. (We’ll come back to that in a second). More importantly, however, GenServer also provides default implementations of the functions demanded by :gen_server, and makes these default implementations overridable (see Line 44). This way, we only have to provide implementations of the functions that we care about, which will “over-ride” the defaults.
Finally, GenServer provides a more pleasing API than what would be required by Erlang. For example, we can call GenServer.start/3 instead of :gen.start/6.
The Final Piece of the Puzzle — Implementing Callbacks
Let’s condense look at a full implementation of a TodoList Genserver. Don’t forget that the expanded code is still there.
As you can see above, we’ve added a few functions to our TodoList module that, broadly speaking, fall into two camps.
First, we added the Client API. These are the functions that we expect clients to interact with. Instead of calling GenServer.call/2, I will be using the more “natural” TodoList.list_tasks/1, and let that function delegate to GenServer.
Secondly, we added the Server API. These are the functions that we expect our server to use as callbacks.
Now, in the source code of GenServer.call, we can see that it delegates to :gen.call/3. As you can imagine, after :gen.call/3 executes successfully, it will call Module.handle_call/3, which in our case is TodoList.handle_call.
I ventured into the Erlang docs themselves to verify as much. I’m still a bit dazed from the excursion, but rest assured, in reference to Module.handle_call/3:
The GenServer abstraction is beautifully simply. We define a module, use GenServer, implement a Client API that delegates to functions defined in GenServer, and implement the callback functions that we are interested in.