Implement a new Erlang OTP behavior

1   Introduction

This article explains and gives guidance on how to implement a new Erlang OTP behavior.

In this document we'll implement a new behavior named gen_example. This new behavior extends the gen_server behavior.

We'll also develop an example user module named gen_example_test01 that uses our new behavior.

We'll see snippets of these two modules in the explanations that follow. And, the complete modules are listed near the end of this document. (see section The complete implementation)

2   Pick a base behavior

We don't want to do all the work ourselves when much of it has been done and tested for us. So we'll pick an existing OTP behavior, one that likely comes with the Erlang OTP distribution, and will "extend" that.

Because it is so generic, gen_server is the most likely choice. This is a good place to start, unless the custom behavior we want to produce is intended to act, for example, as a supervisor, in which case we might extend supervisor , or as an application, in which case we might extend application. For more information on these Erlang OTP behaviors, you can look at the following:

A question -- If gen_server is so generic, then we'd guess that is does not do much for us in the sense of providing any of the functionallity that we might want. So, what do we get by using and extending it? A good answer is that gen_server provides the OTP functionality, in particular, the functionality that enables us to run our code and the code that uses and extends our new behavior within an OTP supervision tree, so that, for example, we can ask a supervisor to restart our code if it should fail and so that our new behavior will automatically support "hot loading" for revisions to our code and the user's extensions to our code.

3   Design the interface

This design, for the most part, requires two things:

  • The API -- We'll implement the functions that our new behavior will expose. These are the functions that can be called by a user module. (see section Implement the API)
  • The callbacks -- We'll declare the callbacks that our new behavior requires the user module to implement. We'll do this by using the -callback directive and the function declaration language/syntax. (see section Specify the callbacks)

4   Implement the API

Our example behavior gen_example will implement and export the functions start_link/1, sync_hello/1, async_hello/1, and stop/0. These are the functions that can be called by a user of our behavior. Here is the code (from gen_example.erl):

%% API
-export([
         start_link/1,
         sync_hello/1,
         async_hello/1,
         stop/0
        ]).
        o
        o
        o
-spec start_link(Module::atom()) ->
        {ok, Pid::atom()} |
        ignore |
        {error, Reason::atom()}.

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

-spec sync_hello(Msg::string()) -> ok.

sync_hello(Msg) ->
    gen_server:call(?SERVER, {hello, Msg}).

-spec async_hello(Msg::string()) -> ok.

async_hello(Msg) ->
    gen_server:cast(?SERVER, {hello, Msg}).

-spec stop() -> ok.

stop() ->
    io:format("(gen_example) stopping~n"),
    gen_server:stop(?SERVER).

Notes:

  • The above are the functions that can be called from the user code, i.e. from the code that uses our behavior (gen_example).
  • Notice how in the above functions, we call functions in gen_server, so that gen_server monitor and supervise the execution of our module, for example if we insert our implementation into a supervisor-worker tree.

The callbacks -- These are the functions that must be implemented by the module that uses our OTP behavior, in this case, by gen_example_test01.

5   Specify the callbacks

Erlang provides a special language for describing functions. You can read about that here: http://erlang.org/doc/reference_manual/functions.html.

Using this language to describe the callbacks that we require the user to implement enables the Erlang compiler to check so as to ensure that the user implements those callbacks.

We specify callbacks using the -callback directive. Here is an example:

-callback my_func(Size::integer(), Desc::string) ->
    ok |
    {error, Reason::atom()}.

Here is the specification of the callbacks required by our new behavior (from gen_example.erl):

-callback sync_hello(Msg::string()) -> ok.

-callback async_hello(Msg::string()) -> Void::any().

6   Call the users callbacks -- implement the API etc

Here is one way to do that:

  1. Implement and expose an API. These are the functions that the user code will call.
  2. In those API functions, call gen_server:call and gen_server:cast and the like.
  3. Those calls to gen_server will result in our callbacks (in our behavior gen_example) to be called.
  4. In our callbacks in gen_example, specifically in handle_call and handle_cast``, call the user's callbacks.

Here is what an API might look like (from gen_example.erl):

-spec start_link(Module::atom()) ->
        {ok, Pid::atom()} |
        ignore |
        {error, Reason::atom()}.

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

-spec sync_hello(Msg::string()) -> ok.

sync_hello(Msg) ->
    gen_server:call(?SERVER, {hello, Msg}).

-spec async_hello(Msg::string()) -> ok.

async_hello(Msg) ->
    gen_server:cast(?SERVER, {hello, Msg}).

-spec stop() -> ok.

stop() ->
    io:format("(gen_example) stopping~n"),
    gen_server:stop(?SERVER).

And here is what our gen_server callbacks might look like. This is where we call the user's callbacks (from gen_example.erl):

-spec init(Args::list()) ->
        {ok, State::tuple()} |
        {ok, State::term(), Timeout::integer()} |
        ignore |
        {stop, Reason::atom()}.

init([Mod]) ->
        {ok, #state{module=Mod}}.

-spec handle_call(Request::any(), From::pid(), State::tuple()) ->
        {reply, Reply::any(), State::tuple()} |
        {reply, Reply::any(), State::tuple(), Timeout::integer()} |
        {noreply, State::tuple()} |
        {noreply, State::tuple(), Timeout::integer()} |
        {stop, Reason::atom(), Reply::any(), State::tuple()} |
        {stop, Reason::atom(), State::tuple()}.

handle_call({hello, Msg}, _From, #state{module=Mod}=State) ->
    Mod:sync_hello(Msg),
    {reply, ok, State}.

-spec handle_cast(Msg::any(), State::tuple()) ->
        {noreply, State::tuple()} |
        {noreply, State::tuple(), Timeout::integer()} |
        {stop, Reason::atom(), State::tuple()}.

handle_cast({hello, Msg}, #state{module=Mod}=State) ->
    Mod:async_hello(Msg),
    {noreply, State}.

Notes:

  • There are additional callbacks that we must define for gen_server, specifically: handle_info, terminate, and code_change. Above, I've only shown the functions that have code specific to our example.
  • In order to call the users callbacks we need to know the module in which they exist. That's the purpose of the Mod variable, and its value, which we saved in our state information in function init and then retrieved in functions handle_call and handle_cast.

7   Call the new behavior's API

In the user module (in gen_example_test01 in our example), we call functions in the new behavior's API (the API exposed by gen_example). Here is an example:

start() ->
    gen_example:start_link(?MODULE).

stop() ->
    io:format("(gen_example_test01) stopping~n"),
    gen_example:stop(),
    ok.

test(Msg) ->
    Msg1 = io_lib:format("1. Msg: ~s", [Msg]),
    Msg2 = io_lib:format("2. Msg: ~s", [Msg]),
    gen_example:sync_hello(Msg1),
    gen_example:async_hello(Msg2),
    ok.

Notes:

  • In the call to gen_example:start_link, we pass an atom that's the name of our user module. The new behavior (gen_example) will need that in order to call our user callbacks.

8   Implement the user callbacks

These are the functions called as a result of our calling functions in the new behavior's API. Example (from gen_example_test01.erl):

sync_hello(Msg) ->
    io:format("(sync_hello) Msg: \"~s\"~n", [Msg]).

async_hello(Msg) ->
    io:format("(async_hello) Msg: \"~s\"~n", [Msg]).

9   The complete implementation

Here are the entire example files.

gen_example.erl:

-module(gen_example).

-behaviour(gen_server).

%% API
-export([
         start_link/1,
         sync_hello/1,
         async_hello/1,
         stop/0
        ]).

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

-define(SERVER, ?MODULE).

-record(state, {module}).

%%%===================================================================
%%% User callback declarations
%%%===================================================================

-callback sync_hello(Msg::string()) -> ok.

-callback async_hello(Msg::string()) -> Void::any().

%%%===================================================================
%%% API
%%%===================================================================

-spec start_link(Module::atom()) ->
        {ok, Pid::atom()} |
        ignore |
        {error, Reason::atom()}.

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

-spec sync_hello(Msg::string()) -> ok.

sync_hello(Msg) ->
    gen_server:call(?SERVER, {hello, Msg}).

-spec async_hello(Msg::string()) -> ok.

async_hello(Msg) ->
    gen_server:cast(?SERVER, {hello, Msg}).

-spec stop() -> ok.

stop() ->
    io:format("(gen_example) stopping~n"),
    gen_server:stop(?SERVER).


%%%===================================================================
%%% gen_server callbacks
%%%===================================================================

-spec init(Args::list()) ->
        {ok, State::tuple()} |
        {ok, State::term(), Timeout::integer()} |
        ignore |
        {stop, Reason::atom()}.

init([Mod]) ->
        {ok, #state{module=Mod}}.

-spec handle_call(Request::any(), From::pid(), State::tuple()) ->
        {reply, Reply::any(), State::tuple()} |
        {reply, Reply::any(), State::tuple(), Timeout::integer()} |
        {noreply, State::tuple()} |
        {noreply, State::tuple(), Timeout::integer()} |
        {stop, Reason::atom(), Reply::any(), State::tuple()} |
        {stop, Reason::atom(), State::tuple()}.

handle_call({hello, Msg}, _From, #state{module=Mod}=State) ->
    Mod:sync_hello(Msg),
    {reply, ok, State}.

-spec handle_cast(Msg::any(), State::tuple()) ->
        {noreply, State::tuple()} |
        {noreply, State::tuple(), Timeout::integer()} |
        {stop, Reason::atom(), State::tuple()}.

handle_cast({hello, Msg}, #state{module=Mod}=State) ->
    Mod:async_hello(Msg),
    {noreply, State}.

-spec handle_info(Info::any(), State::tuple()) ->
        {noreply, State::tuple()} |
        {noreply, State::tuple(), Timeout::integer()} |
        {stop, Reason::atom(), State::tuple()}.

handle_info(_Info, State) ->
        {noreply, State}.

-spec terminate(Reason::atom(), State::tuple()) -> Void::any().

terminate(_Reason, _State) ->
    io:format("(terminate) stopping~n"),
    ok.

-spec code_change(OldVsn::term(), State::tuple(), Extra::term()) ->
        {ok, NewState::tuple()}.

code_change(_OldVsn, State, _Extra) ->
        {ok, State}.

gen_example_test01.erl:

-module(gen_example_test01).

-behavior(gen_example).

% API
-export([
         start/0,
         test/1,
         stop/0
        ]).

% gen_example callbacks
-export([
         sync_hello/1,
         async_hello/1
        ]).

% API

start() ->
    gen_example:start_link(?MODULE).

stop() ->
    io:format("(gen_example_test01) stopping~n"),
    gen_example:stop(),
    ok.

test(Msg) ->
    Msg1 = io_lib:format("1. Msg: ~s", [Msg]),
    Msg2 = io_lib:format("2. Msg: ~s", [Msg]),
    gen_example:sync_hello(Msg1),
    gen_example:async_hello(Msg2),
    ok.

%
% Callbacks
%

sync_hello(Msg) ->
    io:format("(sync_hello) Msg: \"~s\"~n", [Msg]).

async_hello(Msg) ->
    io:format("(async_hello) Msg: \"~s\"~n", [Msg]).

Notes:

  • I added debugging statements to stop and terminate functions so that we could be sure that they were actually being called.

links