16

Trying to define a custom behaviour in Erlang, I cannot figure out a way to apply the callback function inside the behaviour definition module. The compiler claims, the callback function is undefined.

I was expecting a callback function in a behaviour to work just like an abstract method in an OO language where it is possible to use the method without specifying the implementation.

The below example defines a callback function fn. Then, that function is used in add_one. What fn actually does is controlled by the Erlang module implementing this behaviour.

-module( mybeh ).

-callback fn( A::number() ) -> B::number().

-export( [add_one/1] ).

add_one( A ) ->
  1+fn( A ).

But when I try to compile the file mybeh.erl, I get the following error message:

$ erlc mybeh.erl
mybeh.erl:8: function fn/1 undefined

Code examples I found on erlangcentral.org, learnyousomeerlang.com or metajack.im were too simplistic to cover this case. Neither did I have any luck with grepping my way through well known Erlang projects on Github (could have tried harder though).

Jörgen Brandt
  • 210
  • 2
  • 9

1 Answers1

18

You are very close. It is actually easier than what you tried:

-module(my_behavior).

-callback fn(A :: term()) -> B :: term().

The compiler can understand this fully, just as it is.

So easy, it's sort of anticlimactic.

EDIT

"Cool story, how to use?"

Nothing speaks like a working example:

Here we have an abstract service. It is supposed to respond to a very narrow spectrum of messages, and ridicule anything else. Its special element, though, is that it accepts as its startup argument the name of a module which defines some special aspect of its behavior -- and this is the callback module.

-module(my_abstract).
-export([start/1]).

start(CallbackMod)->                
    spawn(fun() -> loop(CallbackMod) end).

loop(CBM) ->
    receive
        {Sender, {do_it, A}} ->
            Sender ! CBM:fn(A),
            loop(CBM);
        stop ->
            io:format("~p (~p): Farewell!~n",
                      [self(), ?MODULE]);
        Message ->
            io:format("~p (~p): Received silliness: ~tp~n",
                      [self(), ?MODULE, Message]),
            loop(CBM)
    end.

So here we define a really simple callback module, in accordance with the behavior defined as 'my_behavior' above:

-module(my_callbacks).
-behavior(my_behavior).
-export([fn/1]).

fn(A) -> A + 1.

Here it is in action!

1> c(my_behavior).
{ok,my_behavior}
2> c(my_abstract).
{ok,my_abstract}
3> c(my_callbacks).
{ok,my_callbacks}
4> Service = my_abstract:start(my_callbacks).
<0.50.0>
5> Service ! {self(), {do_it, 5}}.
{<0.33.0>,{do_it,5}}
6> flush().
Shell got 6
ok
7> Service ! {self(), {do_it, 41}}.
{<0.33.0>,{do_it,41}}
8> flush().                        
Shell got 42
ok
9> Service ! stop.
<0.50.0> (my_abstract): Farewell!
stop

So what good is the behavior definition? It didn't actually do anything! Well, what good are all those Dialyzer type hierarchy declarations? They don't do anything either. But they help you automatically check your work to make sure you won't experience some exciting runtime fail -- but neither Dialyzer nor behavior definitions force you to do anything: they merely warn us of our (likely) impending doom:

-module(my_other_callbacks).
-behavior(my_behavior).
-export([haha_wtf/1]).

haha_wtf(A) -> A - 1.

And when we build this we get:

10> c(my_other_callbacks).
my_other_callbacks.erl:2: Warning: undefined callback function fn/1 (behaviour 'my_behavior')
{ok,my_other_callbacks}

Note, though, that this module was actually compiled and is still independently useable (but not by our abstract service, which is expecting to find fn/1 defined in anything called a my_behavior):

11> my_other_callbacks:haha_wtf(5).
4

Hopefully this little walkthrough sheds some light on the path.

zxq9
  • 13,020
  • 1
  • 43
  • 60
  • 1
    Actually, I need to get the function apply_fn (now add_one) to work. I adapted the example to make it clearer. – Jörgen Brandt Sep 01 '15 at 17:21
  • @JörgenBrandt OK, bit of an edit there... This isn't hard to understand, but to really grok it requires a full example the first time you see it. – zxq9 Sep 01 '15 at 17:52
  • Thanks for the effort invested so far. Shouldn't it be -module( my_behaviour ). in the first line of the first snippet? Or am I missing something? – Jörgen Brandt Sep 01 '15 at 18:01
  • 1
    @JörgenBrandt You're thinking too hard about names. The behavior was already defined in the very first thing we wrote. That's its own file. You have to define an abstract service that expects to use callback modules that adhere to this behavior (that's a second file). Then you have to define callback modules that actually do adhere to the behavior definition (and those are all the next files). Try building the three example modules above -- it will suddenly make more sense once you do it yourself. – zxq9 Sep 01 '15 at 18:06
  • 3
    Now I get it! The behaviour definition is just there to let you know which functions to implement, so that, if you leave one out, the thing crashes at compile time. Thanks for all the effort. – Jörgen Brandt Sep 01 '15 at 18:21
  • @JörgenBrandt ^.^ This sort of abstraction seems to elude some folks forever, so I'm really glad you got it sorted out! Now the OTP behaviors will actually feel much more like your friends than your boilerplate nemeses. – zxq9 Sep 01 '15 at 18:30
  • 2
    Personal opinion here (correct me if I'm wrong), but I believe one could define the "abstract interface" in the behavior module (my_behavior). That way, it would appear less "useless". I believe `gen_server` behavior is implemented this way with the abstract interface and loop function implemented in `gen_server` itself; it also makes calls to the `gen` module as well. – eazar001 Jun 05 '16 at 22:56
  • 1
    @eazar001 You are exactly correct -- and that is a very normal thing to do. Whenever there is a single module that defines the operation of the base behavior this is easier to understand. Occasionally I have a case where the behavior is really a common interface and there are multiple back- _and_ front-ends (mob controller AIs in game servers, for example), and then its nice to have a separate behavior-only declaration. Here I wanted to just illustrate the syntax of callback declarations -- there is no reason for it to be in a separate module. – zxq9 Jun 06 '16 at 08:07