Amtal

Monads in Erlang

The erlando library has been around for a while, but very few people know how to use it. Part of the problem is that monads show up in so many places it’s hard to understand what exactly they are. The other part is a lack of solid examples.

I will make some.

1.0 Removing Repetition

A common sign of a monad’s presence is repetition. If you find a vague pattern showing up multiple times in one function, it can often be extracted1 into a monad.

Suppose your function does a bunch of stuff that might fail, and you want it to return {ok,Result} or {error,Reason}. This shows up while doing I/O, database stuff, input sanitization, you name it.

The difficulty lies in getting the error result out of the top-level function, if it occurs somewhere deep inside.

I’ll show you how it can be done with monads.

1.1 File Example

Here’s a fixed and extended example from a RabbitMQ blog. You can see the full module here.

We’re building a function that does a bunch of ok | {ok,Result} | {error,Reason} operations. If it fails we need the reason – rather than nest cases or catch throws, we use Erlando’s error_m monad.

write_file(Path, Data, Modes) ->
    Modes1 = [binary, write | (Modes -- [binary, write])],
    do([error_m ||
        Bin <- make_binary(Data),
        Hdl <- file:open(Path, Modes1),
        Result <- do([error_m ||
            ok <- file:write(Hdl, Bin),
            file:sync(Hdl)]),
        file:close(Hdl),
        Result]).

We know the representation of error_m – it’s very simple. (Haskell programmers will be reminded of Data.Either. 2)

-type error_m(Result,Reason) :: ok | {ok,Result} | {error,Reason}.
-spec write_file(string(), Input, [atom()]) -> error_m(ok, term()).
-spec make_binary(Input)) -> error_m(binary(), term()).

All the functions used return an error_m monad. The backwards arrows unwrap the monad, from {ok,Result} to just Result. If there’s an {error,Reason} computation stops and the error is returned. Exceptions are not trapped, so bugs won’t be masked.

1> fileop:write_file(".","foo",[]).
{error,eisdir}
2> fileop:write_file("foo.txt",[foo],[]).
{error,{not_iolist,[foo]}}
3> fileop:write_file("foo.txt","foo",[lock]).
{error,badarg}
4> fileop:write_file("foo.txt","foo",[]).
ok

1.2 Opaque Monads

We know the underlying representation of error_m, which makes writing functions that return it easy.

make_binary(List) when is_list(List) ->
    try {ok,iolist_to_binary(List)}
    catch error:_ -> {error, {not_iolist,List}}
    end;

However, we don’t need to. Instead we can use error_m:return/1 and error_m:fail/13 to wrap out result in error_m. These two functions are defined for all monads, and let us wrap values in them to complement the <- operator’s unwrapping.

Note that “return” is a counterintuitive, misleading name for most programmers. You can try to think of it as returning the value into a monad – but something like “wrap” would make more sense at our level.

make_binary(Atom) when is_atom(Atom) ->
    error_m:return(atom_to_binary(Atom,latin1));
make_binary(N) when is_number(N) ->
    error_m:fail({bad_binary,N});
make_binary(Bin) when is_binary(Bin) ->
    do([error_m ||
        % in do-syntax, you don't need to specify the underlying 
        % monad when using return
        return(Bin)]). 

With return and fail, all we need to make the monad implementation completely opaque is define a way to get information out outside of do-syntax.

-spec run_error_m(error_m(Result,Reason)) -> ok | {ok,Result} | {error,Reason}.

This is a common technique, where a large number of operations are allowed within a do context, but only a few for getting a result out of the monad. With it, you can implement mutability4, garbage collection, restricted side effects, and gods know what else…

2.0 Footnotes

1 One of my favorite, ridiculous, examples is the pair list writer in shpider. Instead of writing lists like [(1,2), (3,4), (5,6)], you end up doing:

pairs $ do
  "user" := userName user
  "token" := base64 tok
  "count" := show count
  "skip" := show $ n - (n `mod` count)
  "q" := searchTerm
  ...

It looks like silly code golf on small examples, but once you have to write and maintain dozens of pairs it’s wonderful!

2 Astute readers may notice that unlike Either, there’s a third possibility of just ok. Which means my typespec for write_file should actually return something like error_m(none(), term()).

3 The fail function is disliked in Haskell because many monads define it to throw a horrifying, uncatchable exception. Fortunately error_m doesn’t, and Erlang probably wouldn’t care if it did. Catching byzantine exceptions is Erlang’s forte.

4 I can’t think of a good Erlang example for a mutable monad. (People tend to avoid problems that are hard to implement in Erlang.) This is one idea, and segues into a simple example on using monad transformers…