How would you explain what a gen_server is to a newbie?

One of the first things someone will encounter when learning Erlang are gen_servers - how would you explain what one is to a newbie?

5 Likes

It’s an implementation of a client/server model for processes so you don’t have to :slight_smile:

A gen_server (generic server) implements a client/server model as a behavior. The behavior exists so you can implement your own server utilizing the generic server implementation in gen_server by way of a few callbacks.

gen_server and the parts it builds on top of (such as gen itself) abstract away all the tedious and sometimes gory details of building a generic process abstraction, in this case a server, such that you can focus simply on receiving and responding to messages from clients and in a synchronous context, reducing cognitive load and allowing you to get things done.

While, client/server may conjure up ideas about tcp/udp/ip client/servers, and that’s a good case for gen_servers, it’s certainly not limited to that, nor is it the most common case. Thus, when we say client we merely mean any arbitrary process that wishes to communicate with your generic server, to achieve a result via an idiomatic interface and message protocol.

gen_servers have a wide variety of uses. One obvious one as alluded to above is writing network (tcp, udp, etc.) clients and servers, such that a gen_server starts up and holds on to socket, and then other processes may utilize the socket by going through the gen_server. That actually leads into another behavior, gen_tcp, but we’ll save that for later :slight_smile:

A gen_server first and foremost can be used to provide access to a resource, such as a network socket, as ETS table (key value store), they can serve as concurrency / rate limiters, or something as simple as a counter.

A gen_server sometimes is even used when virtually no communication takes place with other processes sans Erlang/OTP itself. One example would be a gen_server the periodically runs a task. In this example such a server would mostly communicate with itself!

A client can be any process (including other gen_servers) that use the gen_server interface to communicate with your server process.

Perhaps, most importantly when coming from an OOP language is to know what a gen_server is not : an object.

It’s easy to try and wrap your head around the concept of a gen_server (or any gen_* behavior for that matter) by mapping it to objects in OOP. Forget all that. Just remember that server and process part and you’ll be ok :slight_smile:

Now you may be asking yourself, “What’s a behaviour?”… At it’s core, a behavior is merely a module which defines callbacks. A module uses a defined behavior and is expected to define these callbacks. You could say it’s close to the concept of a role in other languages. What’s more is the quintessential example of a behavior, the gen_server!

Once again, not only does it define callbacks, it provides an entire implementation for the model at hand so you can focus on getting things done and not re-inventing the wheel. That said, you may sometimes find a behavior which only defines callbacks, there’s a few in OTP. Can you find them?

Explanations often are not so great without an example, so below is perhaps the simplest gen_server one can implement:

-module(ping_pong).

-behaviour(gen_server).

%% API
-export([start_link/0, ping/1]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2]).

start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

ping(Server) ->
    gen_server:call(Server, ping).

init(_Args) ->
    {ok, {}}.

handle_call(ping, _From, State) ->
    {reply, pong, State}.

handle_cast(_msg, State) ->
    {noreply, State}.

And to put it to use :

1> {ok, Pid} = ping_pong:start_link().
{ok,<0.335.0>}
2> ping_pong:ping(Pid).
pong
8 Likes

This is great, but there is one thing that always confuses newcomers in your example, @starbellyArgs!

init/1 doesn’t receive “Args”, it receives “Arg”… one Arg. And the third parameter on gen_server:start_link/4 should’ve never been called Args. It should be Arg.

People tend to think that you can pass a list of arguments to init and then they end up writing stuff like…

start_link(V1, V2) ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [V1, V2], []).

init(V1, V2) ->
   {ok, #state{field1 = V1, field2 = V2}}.

I think that now that we have maps, the proper way of doing that would be along the lines of…

start_link(V1, V2) ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, #{field1 => V1, field2 => V2}, []).

init(#{field1 := V1, field2 := V2}) ->
   {ok, #state{field1 = V1, field2 = V2}}.

And, of course, if you don’t need to send anything to initialize your server…

start_link(V1, V2) ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, nothing, []).

init(nothing) ->
   {ok, #state{}}.

That would, at least, save me a few rounds of explanations every time I teach gen_server to people.

Related to this: Erlang Oddities - Brujo Benavides - YouTube

4 Likes

I made this mistake. But I’ve to admit that it helped me a lot at first.

Especially because it’s easy yes, that’s why I said OOP could be used to explain a bit about what a gen_server is.

Can the object still be a wrapper to capture the concept behind it? Or is it nonsense for you?

Not that I’m attached to the OO concepts but it could be an entry point.

5 Likes

I don’t think it’s non-sense, but I think that might be a harder approach in the long run. I do like teaching via anti-patterns though :slight_smile: However, I think when you’re first learning Erlang/OTP the best thing you can do per the huge paradigm shift in thinking is try to forget. There’s other concepts you can map to as well, such as a process in your operating system.

I don’t remember who said it, it might have been @rvirding, but I always liked the saying “Erlang is an operating system that comes with a language”. :slight_smile:

5 Likes

That’s the hardest part :stuck_out_tongue_winking_eye:

4 Likes

Yeah, that’s true. I wonder if others have helpful tips on doing that :thinking:

3 Likes

I wouldn’t. I’d point them to the relevant page of Joe’s book (p362, section 22.1 “The Road to the Generic Server”):

“This is the most important section in the entire book, so read it once, read it twice, read it a hundred times - just make sure the message sinks in”

7 Likes

That’s wise, but what if you’re on an airplane and don’t have a copy?! :grimacing:

3 Likes

You’d travel anywhere without it? :astonished:

6 Likes

Almost :smile: what I usually try to say is that “I don’t see Erlang as much as a language with concurrency but as a system with a language”. The idea being that as you soon as you start programming in Erlang, or Elixir or LFE for that matter, you start thinking about the system and how it should fit together and work. Well, you should start thinking like that anyway. :wink:

11 Likes