08-errors

Module 08: Errors

  • Write your team names here:

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

Dealing with errors, take 1

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.)

As we have seen (for example, in the type of parseSome), the possibility of errors can be represented by the Either type.

  • 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).

  • 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.

  • 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.)

    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.

  • Change the eval function to have type String -> IO (), and change it to use interpArith2 instead of interpArith.

    • If there is a parse error, display the parse error with the print function.
    • If there is a runtime error err, display it with putStrLn (showInterpError err).
    • If the interpreter finishes successfully, display the result with print.
  • At this point you should be able to test eval with expressions that only contain +. For example:

    • 2++: should print a parse error.
    • 2+x: should print an error message about x being undefined.
    • 2+3: should successfully evaluate to 5.

Dealing with errors, take 2

  • ROTATE ROLES and write the name of the new driver here:

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).

  • 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?

  • Implement another operator (<<*>>) :: 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.

  • Where have you seen something like this before?

  • Now implement all the Bin cases for interpArith3 using these new operators.

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).

  • Finish the implementation of (>>>=) below. Note that the second argument to (>>>=) is a function which takes an a and returns an Either e b. What does this operator do?
(>>>=) :: Either e a -> (a -> Either e b) -> Either e b
Left e1 >>>= _ = undefined
Right a >>>= f = undefined
  • Now use (>>>=) to implement the Let case of interpArith3.

Dealing with errors, take 3

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.

  • 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.

  • 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.

  • 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.

Feedback

  • 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.