Erlang generic behaviors

In this blog post i will explain somethings about an standard approach of programming  in Erlang programming language called behavior. I assume that reader of this blog post is a newbie Erlang programmer and knows basic language features like message passing.

As a newbie programmer or as an experienced programmer who came from OOP to Erlang world, sometimes it’s hard or boring to understanding Erlang behaviors. In this blog post we will lean:

  • What an Erlang behavior is?
  • How Erlang standard behaviors work?
  • Why we should use standard behaviors?

There is a way to writing new behaviors and next blog post title will be “How to implement new standard Erlang behavior?”

What an Erlang behavior is?

In Erlang, a behavior is a design pattern implemented in a module/library.If you know OOP, It provides functionality in a fashion similar to inheritance in OOP. A behavior is one or more exported function(s) in an Erlang module. Those functions must accept fixed size of type-specified Erlang term as argument and must yield some type-specified Erlang terms as return value. When there is a generic parts for doing somethings, we can implement those generic parts in separate module(s) and for specific parts, we can call some functions called callback-function of other module called callback-module that wrote for those specific parts. Example of an standard Erlang behavior is generic server or gen_server module.

How Erlang standard behaviors work?

Suppose we want an Erlang process with following generic parts:

  • Receive all Erlang messages. Because i don’t want to filling up process’s mailbox.
  • Handle messages that their senders may need a reply message in separate callback-function.
  • Handle messages that their senders don’t need a reply message in separate callback-function.
  • Handle all other messages in separate callback-function.

We can write:

-module(gen_x).
-export([start_link/2]).
 
start_link(CallbackModule, State) ->
    spawn_link(fun() -> loop(CallbackModule, State) end).
 
loop(Mod, State) ->
    Msg =
        receive
            Msg_ ->
                Msg_
        end,
    io:format("~p: Received new message ~tp\n", [self(), Msg]),
    State2 =
        case Msg of
            {request, FromPid, Req} ->
                {State3, Reply} = Mod:handle_request(Req, State),
                FromPid ! Reply,
                State3;
            {event, Event} ->
                Mod:handle_event(Event, State); %% Should yield new State
            _ ->
                Mod:handle_other_messages(Msg, State) %% Should yield new state
        end,
    io:format("~p: New state is ~tp\n", [self(), State2]),
    loop(Mod, State2).

For above behavior, i can write following callback-module:

-module(x_test).
-export([handle_request/2, handle_event/2, handle_other_messages/2]).
 
handle_request(Request, State) ->
    io:format("~p: Got request ~tp\n", [self(), Request]),
    Reply = ack,
    %% State is an Erlang term that we may do some works in callback-functions
    %% based its value and often we will change them in callbacks.
    NewState = State+1,
    {NewState, Reply}.
 
handle_event(Event, State) ->
    io:format("~p: Got event ~tp\n", [self(), Event]),
    State-1.
 
handle_other_messages(Msg, State) ->
    io:format("~p: Got message ~tp\n", [self(), Msg]),
    -(State).

Compile and run codes:

~/Desktop $ erlc gen_x.erl x_test.erl 
~/Desktop $ erl
Erlang/OTP 19 [erts-8.3] [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false]
Eshell V8.3  (abort with ^G)
 
1> Pid = gen_x:start_link(x_test, 0).
<0.62.0>
 
2> Pid ! {event, foo}.
<0.62.0>: Received new message {event,foo}
{event,foo}
<0.62.0>: Got event foo
<0.62.0>: New state is -1
 
3> Pid ! {event, bar}.
<0.62.0>: Received new message {event,bar}
{event,bar}
<0.62.0>: Got event bar
<0.62.0>: New state is -2
 
4> Pid ! {request, self(), baz}.
<0.62.0>: Received new message {request,<0.60.0>,baz}
{request,<0.60.0>,baz}
<0.62.0>: Got request baz
<0.62.0>: New state is -1
 
5> flush().
Shell got ack
ok
 
6> Pid ! qux.
qux
<0.62.0>: Received new message qux
<0.62.0>: Got message qux
<0.62.0>: New state is 1

But it’s better to write some API functions for working with process, instead of directly wrapping and sending messages to it. Add following functions to gen_x.erl and export them:

request(Pid, Request) ->
	Pid ! {request, self(), Request},
	receive
		Reply ->
			Reply
	end.
 
send_event(Pid, Event) ->
	Pid ! {event, Event},
	ok.

Recompile code in runtime and test API functions:

7> c(gen_x). 
{ok,gen_x}
 
8> gen_x:send_event(Pid, event). 
ok
<0.80.0>: Received new message {event,event}
<0.80.0>: Got event event
<0.80.0>: New state is -1
 
9> gen_x:request(Pid, request). 
<0.80.0>: Received new message {request,<0.77.0>,request}
<0.80.0>: Got request request
<0.80.0>: New state is 0
ack

Now i want to write a gen_server’s callback module with functionality like above gen_x’s callback-module:

-module(server_test).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2]).
 
init(Arg) ->
	{ok, Arg}.
 
handle_call(Request, _From, State) ->
	io:format("~p: Got request ~tp\n", [self(), Request]),
    Reply = ack,
	NewState = State+1,
	{reply, Reply, NewState}.
 
handle_cast(Event, State) ->
    io:format("~p: Got event ~tp\n", [self(), Event]),
    {noreply, State-1}.
 
handle_info(Msg, State) ->
    io:format("~p: Got message ~tp\n", [self(), Msg]),
    {noreply, -(State)}.
 
terminate(Reason, State) ->
	io:format("~p: terminating with reason ~tp and state ~tp\n", [self(), Reason, State]),
	ok.

Compile and test code:

Erlang/OTP 19 [erts-8.3] [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false]
Eshell V8.3 (abort with ^G)
1> c(server_test).
{ok,server_test}
 
2> {ok, Pid} = gen_server:start_link(server_test, 0, []).
{ok,<0.67.0>}
 
3> gen_server:cast(Pid, foo).
<0.67.0>: Got event foo
ok
 
4> gen_server:cast(Pid, bar).
<0.67.0>: Got event bar
ok
 
5> gen_server:call(Pid, baz).
<0.67.0>: Got request baz
ack
 
6> Pid ! qux.
<0.67.0>: Got message qux
qux
 
7> gen_server:stop(Pid).
<0.67.0>: terminating with reason normal and state 1
ok
What init/1 does? 

In our gen_x behavior, After calling start_link/2 function, we will give pid of process but in gen_server after giving {ok, State} from init/1, gen_server gives its pid to starter process. In next blog post for writing new behavior we will do this in standard manner. Suppose that you want to make a connection to one remote host and you want to do this in new process and after connecting, you want to interact with that process for sending and receiving data to and from connection. If we want to use gen_x, we have not any callback-function for initializing (testing connection and making socket connection before giving pid to starter process). But in gen_server we can try making connection and if connection made, use socket as State value and if does not make, yield {stop, WhyDidNotConnect} as return value and gen_server gives {error, WhyDidNotConnect} instead of its pid. You can also ignore start of new process by yielding atom ignore from init/1.

What terminate/2 does?

Sometimes you may need to do something before termination. For example if you have registered process to some service in init/1, in terminate/2 you should unregister it.

Why we should use standard behaviors?

We can simply add functionality of init/1 and terminate/2 in our code, So why we should use gen_server or other standard Erlang behaviors?

  • They handle Erlang system messages. With system messages you can activate/deactivate log and have statistics, change state,  change code of process when you changed state structure in code, terminate process in standard manner, etc.
  • 1> {ok, Pid} = gen_server:start_link(server_test, 0, []).
    {ok,<0.62.0>}
     
    2> sys:get_state(Pid).
    0
     
    3> sys:get_status(Pid).
    {status,<0.62.0>,
     {module,gen_server},
     [[{'$initial_call',{server_test,init,1}},
     {'$ancestors',[<0.60.0>]}],
     running,<0.60.0>,[],
     [{header,"Status for generic server <0.62.0>"},
     {data,[{"Status",running},
     {"Parent",<0.60.0>},
     {"Logged events",[]}]},
     {data,[{"State",0}]}]]}
     
    4> ChangeState = fun(_OldState) -> 10 end. 
    #Fun<erl_eval.6.118419387>
     
    5> sys:replace_state(Pid, ChangeState).
    10
     
    6> sys:get_state(Pid). 
    10 
     
    7> sys:terminate(Pid, oh_my_god).
    <0.62.0>: terminating with reason oh_my_god and state 10
     
    =ERROR REPORT==== 22-Feb-2018::18:09:29 ===
    ** Generic server <0.62.0> terminating 
    ** Last message in was []
    ** When Server state == 10
    ** Reason for termination == 
    ** oh_my_god
  • They support Erlang process hibernation.
  • They make safe call request. In our gen_x behavior, what if server process dies and we make a call to it? We will wait forever ! What if we send one message from server process to client process and client process without receiving that message sends a request to server? Client process will give that message as reply of its request ! Erlang standard behaviors in their API functions use Erlang gen.erl module that provides safe call for them. In next blog post we will learn how it works.
  • They have been tested, optimized and maintained for years.

Leave a Reply

Your email address will not be published. Required fields are marked *