[erlang-questions] Design patterns?

Richard A. O'Keefe ok@REDACTED
Wed Mar 4 02:02:34 CET 2015


On 4/03/2015, at 6:44 am, Judson Lester <nyarly@REDACTED> wrote:

> On Tue, Mar 3, 2015 at 12:30 AM Gordon Guthrie <gguthrie@REDACTED> wrote:
> Generally function heads are 'better' than cases, particularly if there are a lot of them...
> 
> This email would be easier to answer if there was some code so people could outline options...
> 
> I deliberately didn't include any code because I was more interested in talking about the overall idea ("do erlangers discuss design patterns?") than "it'd be better if you..."

Discussing "overall ideas" without examining concrete cases
is like arguing about orchestration without looking at scores: futile.

If we are going to pontificate about airy nothings, let me remind you
of the MASTER RULE:

   The aim of programming is to COMMUNICATE.
   So do whatever is clearest in context.

That's it.  If we want to make any progress past that, we *have* to
examine *real* code that someone seriously intends to use.

And of course things depend on the current state of the language.
What was the best style at one time may cease to be so when something
better becomes available.

Let's take "view patterns" from Haskell as an example.

Let's look at the classic back-to-back list implementation of queues.

data Queue t = Queue [t] [t]

left_view :: Queue t -> Maybe (t, Queue t)

left_view (Queue (e:es) bs) = Just (e, Queue es bs)
left_view (Queue []     bs) = case reverse bs of
                                (e:es) -> Just (e, Queue es [])
                                []     -> Nothing

Now suppose we want to fold over the elements of a queue.
(Yes, I know this is not the best way to do it.)

fold :: (a -> b -> a) -> a -> Queue b -> a
fold f x q = case left_view q of
               Nothing -> x
               Just (e, q') -> fold f (f x e) q'

Frankly, it would be silly to invent a new function just to replace
the case expression here.  That function would only be _useful_ as
part of the definition of fold.

Now view patterns are patterns of the form (function -> pattern),
with the semantics that the function expression is evaluated and
applied to whatever you are matching, and then the result is
matched (or not) by the pattern.  With that extension, you can write

fold f x (left_view -> Nothing)     = x
fold f x (left_view -> Just (e, q)) = fold f (f x e) q

While it looks as though the function will be evaluated twice, we
expect the compiler to optimise that to a single call.  The code we
get from each version of fold should be pretty much the same.

If Erlang had a class of functions known to the compiler to be pure,
it could have view patterns too, and then the question about whether
to use case or not would often have a different answer.

By the way, the 'case' in left_view is also an example where I think
it would be silly to introduce a new function.

So there are two questions:

* Does the language include view patterns (or abstract patterns)?
* Does the case *make sense* as a separate function?

> I think I was coming to the (general?) conclusion about case statements: they're an intermediary step that are only occasionally left as-is.

Wrong answer.  The right answer is the UNIVERSAL EXPERT ANSWER:

	It all depends.

Sometimes 'case' will be good.  Sometimes it won't.  IT ALL DEPENDS.

By examining REAL CONCRETE EXAMPLES we may be able to induce some guidelines.
Without examining real concrete examples, we're chasing (and swallowing) wind
and end up producing only wind.

> The question remains though: do erlangers discuss design patterns? (I see a lot of "I'd do it this way..." which seems like sort of the same thing.)

When the design patterns book came out, the reaction of many Lispers and Smalltalkers
are "design patterns are what you talk about when your language makes everything look
hard".  Half fun and full earnest. 

Let's look at some examples.

format_exception({Class,Term,Trace})
  when is_atom(Class), is_list(Trace) ->
    case is_stacktrace(Trace) of
        true ->
            io_lib:format("~w:~P\n~s",
                          [Class, Term, 15, format_stacktrace(Trace)]);
        false ->
            format_term(Term)
    end;
format_exception(Term) ->
    format_term(Term).

is_stacktrace/1 is a function that examines a list checking that
every element is {atom,atom,integer} or {atom,atom,list}.   If we had

-pure([is_stacktrace/1]).

and view patterns that relied on checked purity, this could be

format_exception({Class = (is_atom->true), Term, Trace = (is_stacktrace->true)}) ->
    io_lib:format("~w:~P\n~s", [Class, Term, 15, format_stacktrace(Trace)]);
format_exception(Term) ->
    format_term(Term).

Or even if -pure functions were allowed in guards, this could be

format_exception({Class,Term,Trace})
  when is_atom(Class), is_stacktrace(Trace) ->
    io_lib:format("~w:~P\n~s", [Class, Term, 15, format_stacktrace(Trace)]);
format_exception(Term) ->
    format_term(Term).

The constraint for a pure function is that it may not contain
send or receive and may only call pure BIFs and other pure functions.
Ideally it should be obviously terminating.

dlist_flatten(Xs) ->
    case dlist_next(Xs) of
        [X | Xs1] -> [X | dlist_flatten(Xs1)];
        [] -> []
    end.

This is a textbook example of where a view pattern would be just right:

dlist_flatten((dlist_step -> [X|Xs1])) ->
    [X | dlist_flatten(Xs1);
dlist_flatten((dlist_step -> [])) ->
    [].

except that there are much better ways to achieve the same end.

fun_parent(F) ->
    {name, N} = erlang:fun_info(F, name),
    case erlang:fun_info(F, type) of
        {type, external} ->
            N;
        {type, local} ->
            S = atom_to_list(N),
            list_to_atom(string:sub_string(S, 2, string:chr(S, $/) - 1))
    end.

I'm quite unhappy about this function because it is strongly coupled to
the precise form of the atom returned by fun_info(_, name) for
local functions, and that is not documented in
http://www.erlang.org/doc/man/erlang.html#fun_info-1
A comment explaining what the format is assumed to be is needed here.
That would do a lot to improve the understandability of this code;
splitting the 'case' out as a separate function would, if anything,
worsen it.  Now splitting out

parent_from_local_name(N) ->
    list_to_atom(string:sub_string(S, 2, string:chr(S, $/) - 1)).

*would* improve readability, especially given that vital comment.

try_apply(F, Arg) ->
    case erlang:fun_info(F, arity) of
        {arity, 1} ->
            {module, M} = erlang:fun_info(F, module),
            {name, N} = erlang:fun_info(F, name),
            try_apply(F, Arg, M, N);
        _ ->
            {error, badarity}
    end.

It's not clear to me why this wasn't

try_apply(F, Arg)
  when is_function(F, 1) ->
    {module, M} = erlang:fun_info(F, module),
    {name, N} = erlang:fun_info(F, name),
    try_apply(F, Arg, M, N);
try_apply(_, _) ->
    {error, badarity}.



I have a rule of thumb.
Pick ANY local property of code.
Examine a non-trivial body of code, looking only at matches for that property.
You WILL find things that could be improved.

"Uses 'case'" is a property of this kind.
So is "contains the letter Z".





More information about the erlang-questions mailing list