As part of the rewrite of my Haskell sound change applier, I’ve been creating custom exceptions in Haskell. Admittedly, this way of doing this is discouraged in Haskell, but for some cases it seems like the best approach to me. Namely, the AST may have been transformed in a number of ways before an execution error occurs, and before bailing I want to give an idea about what exactly went wrong. The problem is that to print something understandable before quitting requires access to parts of the top-level interpreter state, and I don’t want to thread it all the way down to the lowest levels of my program.
Anyway, this has caused my two problems. The first is that the top-level of my interpreter is monadic code with the type StateT… IO, which means that I need to lift catch to work in a monad other than IO. I found libraries like monad peel for doing this, but in the end I found doing it myself to be easier for my particular use case, especially since then it’s clear to me exactly what the information flow is going to be when an error occurs. The important thing is that the error handler needs access to the last valid program state to do the correct thing.
The second problem turned out to be the more painful of the two. I found that error handlers weren’t working correctly if they tried to access the program state, but would work just fine as long as they left it alone. Worse, the same error would start printing multiple times before failing, as one or two error handlers in a row tried to catch the error then failed. I eventually decided that this was due to excessive laziness – the error wasn’t being raised in the correct place, and then when it was raised a valid program state didn’t exist. This meant that the error handler failed, which then caused the error to propagate upwards again and again.
However, just reducing the return value to weak head normal form wasn’t enough. Some of the functions return transformed ASTs, and the error may occur when processing a sub-tree. Therefore, to get the right behaviour, some cases require the entire output data structure to be evaluated within the catch. Eventually, after much fiddling, I finally got it to consistently exhibit the correct behaviour.
So the moral of the story is, beware of lazy evaluation when mixed with anything remotely imperative, like Exceptions. I already knew that intellectually, but obviously my laziness intuition isn’t yet well developed enough to spot this particular problem before it bit me.
Posted by chrisdb on 2011-02-26
Comments