One of the first things someone will encounter when learning Erlang are gen_servers - how would you explain what one is to a newbie?
It’s an implementation of a client/server model for processes so you don’t have to
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
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
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
This is great, but there is one thing that always confuses newcomers in your example, @starbelly … Args
!
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
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.
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 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”.
That’s the hardest part
Yeah, that’s true. I wonder if others have helpful tips on doing that
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”
That’s wise, but what if you’re on an airplane and don’t have a copy?!
You’d travel anywhere without it?
Almost 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.