I've been playing more with this Erlang factoring technique. As an exercise, I've been trying to force myself to adopt the method, by writing functions that are 3 or less lines long (function clauses actually, so multiple pattern-matched claues are OK).
One place I noticed was causing myself
to exceed three lines was receive
statements. So I came up
with this helper function:
receive_to_fun(Fun) ->
receive
Message ->
Fun(Message)
end.
So the question comes up, with this function, is there
any reason (other than selective receive) to write another
receive
statement yourself? Here's a before-and-after:
%% before
handle_receive() ->
receive
{ok, token} ->
%% handle token
ok;
{ok, stop} ->
%% handle stop
ok;
{error, Error} ->
%% handle Error
Error
end.
%% after
handle_receive() ->
receive_to_fun(fun handle_receive_message/1).
handle_receive_message({ok, token}) ->
%% handle token
token;
handle_receive_message({ok, stop}) ->
%% handle stop
stop;
handle_receive_message({error, Error}) ->
%% handle error
Error.
(On a personal note, I also like that this allows us to avoid some of the less desirable parts of Erlang syntax :)
One of the nice things about functional programming is that we often take common patterns and extract them out into higher order functions, like map, foldl, etc. By writing these tiny functions, I've found that I define more of my own (hopefully reusable) higher-order functions.
Here's another before and after example. This is a snippet from a thread ring program.
%% before
go(Pid, 0) ->
Pid ! stop,
receive
stop ->
stop
end.
go(Pid, NumTimes) ->
Pid ! token,
_ = receive
token ->
ok
end,
go(Pid (NumTimes - 1)).
%% after
%% first define our HOF
send_and_receive(Pid, Message, ReceiveFun) ->
Pid ! Message,
receive_to_fun(ReceiveFun).
go(Pid, 0) ->
send_and_receive(Pid, 0, fun handle_go_receive/1),
0;
go(Pid, NumTimes) ->
send_and_receive(Pid, NumTimes, fun handle_go_receive/1),
go(Pid, (NumTimes - 1)).
handle_go_receive(stop) -> ok.
handle_go_receive(token) -> ok.
What we've done is identified sending a message
and immediately waiting for a message as a pattern,
and turned it into a function, send_and_receive
.
I'd be curious if in a larger code-base if a smallish set of these HOF would be reused, or if most of them would only be used once. If they're only used once, are they worth it?
Here's the thread ring program I wrote in this style.
-module(ring).
-compile(export_all).
receive_to_fun(Fun) ->
receive
Message ->
Fun(Message)
end.
send_and_receive(Pid, Message, ReceiveFun) ->
Pid ! Message,
receive_to_fun(ReceiveFun).
start_procs(NumProcs, ProcFun) ->
start_procs(NumProcs, NumProcs, ProcFun, self()).
start_procs(_NumProcs, 0, _ProcFun, Pid) ->
Pid;
start_procs(NumProcs, CountDown, ProcFun, Pid) ->
NewPid = spawn(fun () -> ProcFun(Pid) end),
start_procs(NumProcs, (CountDown - 1), ProcFun, NewPid).
go(Pid, 0) ->
send_and_receive(Pid, 0, fun handle_go_receive/1),
0;
go(Pid, NumTimes) ->
send_and_receive(Pid, NumTimes, fun handle_go_receive/1),
go(Pid, (NumTimes - 1)).
handle_go_receive(_M) -> ok.
proc_fun(Pid) ->
ReceiveFun = fun (M) -> handle_proc_fun_receive(Pid, M) end,
receive_to_fun(ReceiveFun).
handle_proc_fun_receive(Pid, 0) ->
Pid ! 0,
0;
handle_proc_fun_receive(Pid, Number) when is_integer(Number) ->
Pid ! Number,
proc_fun(Pid);
handle_proc_fun_receive(_Pid, Error) ->
Error.
%% here is the entry function
start(NumProcs, NumTimes) ->
ProcFun = fun proc_fun/1,
Pid = start_procs(NumProcs, ProcFun),
go(Pid, NumTimes).
The behavior of the original
handle_receive/0
is different as compared to usingreceive_to_fun/1
. The original will leave unmatched messages in the mailbox (selective receive). Thereceive_to_fun/1
version checks each message and will die on unmatched messages. This isn't a bad thing but something to keep in mind.