Erlang/OTP behaviors for beginner

Rather than try to address your specific questions as other answers have already done, I’ll try to explain in simple terms the basics behind behaviors, and let you answer your own questions based on understanding those basics.

A behavior is basically a message handling framework, where by “framework” I mean the classical definition of a partial solution to a problem that can be completed and customized by the end user. OTP behaviors essentially supply:

  • a message loop
  • integration with underlying OTP support for code upgrade, tracing, system messages, etc.

Behaviors delegate message handling to callback modules, or behavior implementations as “Erlang and OTP In Action” calls them. Upon the invocation of its init/1 function, the callback module generally creates state for the message loop to keep on its behalf. The behavior loop then passes this state to each subsequent invocation of a callback module message handling function, and each of these invocations can return a modified state. Callback functions also return instructions telling the behavior message loop what to do next.

Here’s a greatly simplified version of the message loop at the heart of a behavior:

loop(Callbacks, State) ->
  {Next, NState} =
 receive
                     M1 ->

                       Callbacks:handle_m1(M1,State);
                     M2 ->
                       Callbacks:handle_m2(M2,State);
                     Other ->
                       Callbacks:handle_other(Other,State)
                   end,
  case Next of

    stop -> ok;
    _ -> loop(Callbacks, NState)
  end.

This tail-recursive loop has the Callbacks module and the State variable as arguments. Before this loop is first invoked, you’ve already told the behavior what your callback module is, and then base OTP behavior support code has already called your init/1 callback function to get the initial value of State.

Our example behavior loop receives messages of form M1, M2, and any other message, the details of which don’t matter here, and for each message, invokes a different callback function in the Callbacks module. In this example, the handle_m1 and handle_m2 callback functions handle messages M1 and M2 respectively, while the callback handle_other handles all other kinds of messages. Note that State is passed to each callback function. Each function is expected to return a tuple with the first element telling the loop what to do next and the second element containing possible new state for the loop — either the same value as State or a new different value — which the loop stores in its variable NState. In this example, if Next is the atom stop, the loop stops, but if it’s anything else, the loop invokes itself recursively, passing the new state NState to the next iteration. And since it’s tail recursive, the loop won’t ever blow out the stack.

If you dig through the sources of standard OTP behaviors such as gen_server and gen_fsm, you’ll find a loop much like this, but they’re much more complex due to handling system messages, timeouts, tracing, exceptions, etc. Standard behaviors also start their loops in a separate process, so they also contain code for starting the loop process and passing messages to it.

Leave a Comment