Friday 24 January 2014

Evaluator with Error Handling - Monadic Version

On this basis then we put the bind and unit functions in their own module, which looks like this:

-module(exception_monad).
-export([unit/1, bind/2]).
-export([raise/1]).

unit(A) ->
    {ok, A}.

bind({ok, A}, F) ->
    F(A);
bind({error, E}, F) ->
    {error, E}.

raise(E) ->
    {error, E}.

So here we have a function unit/1 that takes a basic value and wraps it in our tuple.  And the function bind/2 does the fork business - it applies the function F if the value is flagged as ok, but if the value is flagged as
an error it passes the error on unchanged.  The function F is going to be our eval function but here in our monad code we leave this function to be anything that returns a value of the right type.

In addition we have a function raise/1 that creates the error case of our value.  Logic says that this code belongs in here.

So now we re-write the evaluator with error to make use of the monad code as follows:

-module(eval_em).
-export([eval/1]).
-export([answer/0, error/0]).

-import(exception_monad, [unit/1, bind/2, raise/1]).

% Evaluator with exception monad

eval({con, A}) ->
    unit(A);
eval({dv, T, U}) ->
    bind(eval(T), fun(A) ->
      bind(eval(U), fun(B) -> 
        if
          B == 0 -> raise("divide by zero");
          true -> unit(A div B)
        end
      end)
    end).

answer() ->
    {dv, {dv, {con, 1972}, {con, 2}}, {con, 23}}.

error() ->
    {dv, {con, 1}, {con, 0}}.

We use the import command to get the functionality from our monad module, which is compiled separately.

Now our evaluation function can deal with the case of a simple value by calling unit/1 to turn this into a monadic value.  And it deals with an expression to evaluate - it ...evaluates the first expression and then with this value in hand it
...calls the function that
...evaluates the second expression and then with both values in hand it
...calls the function that
...either works out the sum or raises an error as appropriate.

All of this happens inside one function call.  The sequence is just the sort of regular pattern that you can smooth over with a bit of syntactical custard.

Also I like the fact that our client code does not know or need to know how the monadic value is constructed - it does not need to know that the value is created by using a tuple and the two atoms ok and
error.  All it needs to do is call the right functions, raise and unit, to create the values it wants.

Does this work as previously?

Eshell V5.10.2  (abort with ^G)
1> l(eval_em).
{module,eval_em}
2> eval_em:eval(eval_em:answer()).
{ok,42}
3> eval_em:eval(eval_em:error()).
{error,"divide by zero"}

So it does.

No comments:

Post a Comment