Reading Haskell – Part 4

In the last three articles I covered the overall structure, lexer, and parser of a simple expression evaluator. This articles concludes by presenting the evaluator and main loop.

At this stage we are able to take a string, tokenize it, and then build a tree representing the expression. We now need to be able to reduce the tree down to a single value (or error).

Expression Reduction

reduce :: (MonadError ExprError m) => ExpressionTree -> m Int

We’re returning an Int, but as before, the context for this return value is an error state.

The reducer is a really simple recursive function. Leaf values in the tree (numbers) are their own value:

reduce (Node (NNumber _ v) _)
    = return v

Operator nodes recursively reduce their left and right child nodes, then apply the operator:

reduce (Node (NOperator p _ op) (lhs:rhs:[]))
    = do x <- reduce lhs
         y <- reduce rhs
         case op x y of
             Right v -> return v
             Left m  -> throwError $ ExprErrorAt m p

At this point we’re “collapsing” the error context. Previously we just defined it as any type that satisfied the ‘MonadError‘ interface. In this function, we force it to be the ‘Either‘ type by expecting the return value to be either ‘Right‘ (success) or ‘Left‘ (failure). Type inference sorts everything out nicely here.

Main Loop

Everything I’ve presented here has been ‘pure’ – free of side effects. That’s all well and good, and it lets us reason about the program, but ultimately we have to deal with the computer and the user, and that’s all about side-effects. The main function is the only bit of impure code in this whole example:

main = interact $ unlines . map (unlines . formatResult) . lines

The period operator is the function composition operator, so we read from right to left.

In other words: convert the input to a list of lines, process each one through ‘formatResult‘ (our function), concatenate our multi-line output, and concatenate again to produce a single string for output. The standard function ‘interact‘ handles all the messy business of reading and writing from the console streams.

Examples

2 + 3 * 4
14

(12+34)*(56+78)
6164

123 / (4 - 4)
** Error in expression:
**   123 / (4 - 4)
**       ^
** Reason: division by zero

123 +
** Error in expression:
**   123 +
**        ^
** Reason: expected a value

Leave a Reply