In this module, we will explore how to elegantly deal with errors that come up during parsing, typechecking, or at runtime.
Begin by coping in your module 7 code from the previous module:
COPY YOUR MODULE 7 CODE HERE
The interpreter works, but we have introduced the possibility of runtime errors, and we are not dealing with them in a nice way: if a variable is undefined, the interpreter simply crashes. Ideally, the interpreter should never crash, but instead return a useful error value that can be caught and further processed however we wish.
To do this we will need to change the type of the interpreter again. Currently, its type promises that it will always return an Integer, but this is no longer possible: in case of a runtime error it will not be able to return an Integer. (Of course, we could arrange for it to return some default Integer such as 0 in the case of a runtime error, but there would then be no way to distinguish between an expression that legitimately evaluated to 0 and one that resulted in a runtime error.)
Integer
As we have seen (for example, in the type of parseSome), the possibility of errors can be represented by the Either type.
parseSome
Either
Create a new data type called InterpError to represent runtime errors generated by the interpreter. For now, it should have only one constructor called UndefinedVar, containing a String (which will store the name of the variable that is undefined).
InterpError
UndefinedVar
String
Our goal is to change the type of interpArith so that it returns Either InterpError Integer instead of just Integer. However, at this point if we simply change its type it will not typecheck.
interpArith
Either InterpError Integer
Create a new function interpArith2 which has the same type as interpArith except that it returns Either InterpError Integer instead of Integer. For now, just make interpArith2 handle Var, Let, Bin Plus, and Lit. (Don’t bother implementing Bin Minus or Bin Times; they would be very similar to Bin Plus.)
interpArith2
Var
Let
Bin Plus
Lit
Bin Minus
Bin Times
WARNING: this will be very annoying to write. But unless you write this code you will not appreciate the nicer way we will implement it next!
Write a function showInterpError :: InterpError -> String which displays a nice (human-readable) version of interpreter errors.
showInterpError :: InterpError -> String
Change the eval function to have type String -> IO (), and change it to use interpArith2 instead of interpArith.
eval
String -> IO ()
print
err
putStrLn (showInterpError err)
At this point you should be able to test eval with expressions that only contain +. For example:
+
2++
2+x
x
2+3
5
The annoying thing about interpArith2 is that it had to mix together the actual work of interpreting with the work of doing case analysis to figure out when a runtime error had occurred. In this section we will explore ways of hiding all the case analysis inside a few general combinators which we can then use to implement the interpreter in a nicer way.
Start by creating a function interpArith3 with the same type as interpArith2. For now just implement the Lit and Var cases (you can probably just copy them from interpArith2).
interpArith3
Now implement an operator (<<$>>) :: (a -> b) -> Either e a -> Either e b. (If your implementation typechecks, it is probably correct. Note you should define it directly in this file, not in Parsing.hs.) What does this operator do?
(<<$>>) :: (a -> b) -> Either e a -> Either e b
Parsing.hs
Implement another operator (<<*>>) :: Either e (a -> b) -> Either e a -> Either e b.
(<<*>>) :: Either e (a -> b) -> Either e a -> Either e b
Now try things like
(+) <<$>> Right 2 <<*>> Right 5
(+) <<$>> Left "nope" <<*>> Right 6
(+) <<$>> Right 2 <<*>> Left "uhuh"
(+) <<$>> Left "nope" <<*>> Left "uhuh"
Explain what the pattern f <<$>> ... <<*>> ... <<*>> ... does.
f <<$>> ... <<*>> ... <<*>> ...
Where have you seen something like this before?
Now implement all the Bin cases for interpArith3 using these new operators.
Bin
Now we only have the Let case remaining. It turns out that (<<$>>) and (<<*>>) are not enough to implement the Let case, since the two recursive calls to interpArith3 are not independent (the second recursive call needs to use an environment extended with the result of the first recursive call).
(<<$>>)
(<<*>>)
(>>>=)
a
Either e b
(>>>=) :: Either e a -> (a -> Either e b) -> Either e b Left e1 >>>= _ = undefined Right a >>>= f = undefined
As you have probably noticed, (<<$>>) and (<<*>>) are similar to the (<$>) and (<*>) operators for parsers. In fact, they are both instances of a more general pattern.
(<$>)
(<*>)
Delete the line import Prelude hiding ((<$>), (<$), (<*>), (<*), (*>)). Prelude is always imported implicitly, so we are now importing the more general versions of those operators.
import Prelude hiding ((<$>), (<$), (<*>), (<*), (*>))
Prelude
Change the import of Parsing to Parsing2. You can obtain Parsing2.hs here. Parsing2 is identical to Parsing except that it does not export specialized versions of the combinators.
Parsing
Parsing2
Now change all the uses of (<<$>>) and (<<*>>) in interpArith3 to (<$>) and (<*>). Confirm that your code still typechecks and behaves the same way.
Look at the types of (<$>) and (<*>) in GHCi. You can see that they have the same shape as the specific versions for Parser and Either, but work over any type f which supports the same sort of pattern.
Parser
f
It turns out that (>>>=) corresponds to something more general too: change the use of (>>>=) to (>>=), and confirm that your code still works.
(>>=)
As a final comprehensive exercise, extend the language with a division operator. (You can use the Haskell div function to perform integer division.) The interesting thing about division is that it introduces the possibility of a different kind of runtime error, namely, division by zero. Extend the interpreter so that it does not crash but instead generates an appropriate error when division by zero is attempted.
div
How long would you estimate that you spent working on this module?
Were any parts particularly confusing or difficult?
Were any parts particularly fun or interesting?
Record here any other questions, comments, or suggestions for improvement.