public
Created

  • Download Gist
erl_factor.markdown
Markdown

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).

Death to receive expressions

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 :)

Common patterns to higher-order functions

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?

Some more code

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 using receive_to_fun/1. The original will leave unmatched messages in the mailbox (selective receive). The receive_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.

@dreverri, yes, great point. Both should probably be extended to handle other types of messages and handle the error appropriately.

Nice @reiddraper I finally got around to reading Gar1t's post and saw your link on the right side. Coming from an imperative background it is hard for me to write things like his final output, but when I see how easy it is to test and refactor it makes me want to write more erlang.

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.