How I behave
First off: I am not a type fan-boy when it comes to Erlang/OTP, so if you have a problem with fast-n-loose Erlang source-code, this post may not be for you.
Secondly: I tend to use tools in slightly off ways. If you want to learn how to actually use Erlang behaviors properly, google will be your friend to pan through the half-dozen mailing-list examples, and blog-posts about creating your own custom Erlang/OTP behaviors.
Given all that, I want to further preface this post with a great nugget from an Erlang regular: @jlouis (or, in this case, his GitHub identity, ‘I GIVE CRAP ANSWERS’).
Custom behaviors allow you to specify a contract. This contract is given by a list of function names/arities that must exist in a module implementing that behavior.
As an example, you could define a module which has ‘gen_server‘ behavior and then omit the ‘handle_info/2‘ function from it. The behavior-check will then error out because you are missing part of the contract.
That is all there is to it! To implement them, one defines a special function in the behavior-defining module, ‘behaviour_info/1‘ which tells the Erlang system about behaviors.
How I behave
Not repeating yourself is central to modern code cleanliness. Abstracting code that can be reused in many places is just the ground floor, though. Sometimes within that general-case code, we may want to calcify its behavior by enforcing how it is used. I think of these behaviors as Erlang contracts, though the OTP team just calls them behaviors.
To get these contracts to interplay with their calling code modules doesn’t take much effort at all. Traditionally, all one had to do is export a special routine from that contract module, and yield a term that designated what is to be used. Here is an example contract module:
-module(foo_contract).
% behavior routines
-export([behaviour_info/1]).
% api routines
-export([new/1, new/2]).
-ifdef(debug).
-define(CMD, "csh -fv priv/scripts/hook.csh").
-else.
-define(CMD, "csh -f priv/scripts/hook.csh").
-endif.
%%
%% behavior routines
%%
behavior_info(callbacks) -> [{new, 1}];
behavior_info(_) -> undefined.
behaviour_info(A) -> behavior_info(A).
%%
%% contract routines
%%
new(Module) ->
{ok, U} = handle_cmd(),
handle_new(Module, U).
new(Module, A) ->
{ok, U} = handle_cmd(A),
handle_new(Module, U).
%%
%% business routines
%%
handle_new(Module, A) ->
U = Module:new(A),
handle_callback(U). % N.B. enforce shape
handle_callback({ok, U}) -> {ok, U}.
handle_cmd() ->
{ok, ?CMD}.
handle_cmd(As) ->
U = lists:join([32], As),
V = [?CMD, [32], U],
{ok, V}.
You will see that there are two spellings of the magical ‘behavior_info’ routine, and that’s important: the non-UK version is just for our internal house-keeping, but the other version is the one that the compiler/runtime expects.
This module, when called, sets up some args for a ‘hook.csh’ script, then passes that string (list) to a callback routine that it has received as an arg. This dynamic function call is how we can make this all general-case, with the added benefit of maintaining a bare-minimum amount of control of what the caller should expect.
The ‘Module’ that is passed could be anything. There could be many flavors of it. But here is the callback module that we will use in this example:
-module(foo_callback).
-behavior(foo_contract).
% contract routines
-export([new/1]).
%%
%% contract routines
%%
new(A) ->
U = exe(A),
{ok, U}.
%%
%% support routines
%%
exe(A) ->
% N.B. could spawn a new process like:
% proc_lib:spawn(os, cmd, [A])
U = os:cmd(A),
U.
So here you will see a mechanism whereby because
we have used the directive ‘behavior’ which is
tethered to the module ‘foo_contract,’ and because
‘foo_contract’ has a ‘behaviour_info’ term of {new, 1}
then it must be the case that our callback module
exports a routine of ‘new/1.’
(N.B. the behavior directive can have either spelling in this case. Go figure.)
And without wiring this all together, we have code that will compile and play nice. This custom behavior is good to go, even though we haven’t really done much at all.
But even though we haven’t done much, there is something powerful going on. We have generalized a code-chunk for re-use; we have ensured that calling code provides a callback that adheres to the contract that we have specified for it; we have made it clear to future developers that there is a plug-n-play system in place to tap into.
The only bit we have left out is the calling code, that could live anywhere, to be honest. It could even just live in the Erlang shell. But for completeness, here is a module that ties this all together:
-module(foo).
-export([start/0]).
start() ->
U = ["foo", "bar", ".jpg"],
{ok, _} = foo_contract:new(foo_callback, U),
ok.
Here we are passing in a list of lists to our contract with the included callback module. Again, the entire mechanism works because of the callback module being tethered to what the contract module deems it will be working with. So, with no clue of all the inner workings, with a little API document describing how one can tap into this engine, a developer can just take it all for granted, know that if he passes a list of shell args in a call to the following:
foo_contract:new(foo_callback, List).
… everything will work out fine.
In this example, we have taken for granted what the ‘hook.csh’ script does. Perhaps it takes an args-list of file names then appends a ‘jpg’ suffix to each (the last arg). It hardly matters. What matters is knowing that custom behaviors can be utilized to abstract and guarantee your code where it matters.
Update Oct. 2024
@mononcqc was good enough to correct me that ‘I GIVE CRAP ANSWERS’ is @jlouis (Jesper Louis Andersen). When I first posted, I wrote that @zxq9 was perhaps the user behind the ‘I GIVE CRAP ANSWERS’ account on GitHub.