A languages exception mechanism is important to understand as they are vital to building software which can work with the outside world. Haskell’s laziness creates some problems for dealing with exceptions that you need to be aware of. Specifically it can be difficult to reason about exactly when a code will be evaluated and when it can escape the scope of an exception handler. Consider the following:
Now of course since haskell is lazy the evaluation of iThrowExceptions, which can throw exceptions is going to be deferred until we actually use its value. Another example is assigning records in a field:
In this case since mrField is not a primitive decodeUtf8 is once again not going to be evaluated within the exception handler. Even if we are in IO we will only evaluate into Weak Head Normal Form so our list is still going to be thunks.
I am going to show two practices that we use in our code to help with proper exception scoping. The first is safeCatch. The problem with catch is that it does not guarantee that the evaluation will take place within the exception handler. Why the decision to have exception handlers travel with thunks was not made I do not know, but we live in a world where they do not. Fortunately Control.DeepSeq provides us a mechanism to ensure that our evaluation is done at a specific point and our data structures will have no thunks in them. The core of DeepSeq is a typeclass called NFData. The implementation of the rnf function fully evaluated our data type and then recurses through its children to evaluate any thunks they contain. Now implementing NFData instances for all our types would involve lots of boilerplate code so we can use a package like deepseq-th to make instances for us. Internally we have our own implementation for historical reasons. I would like to see a move made to Generics and away Template Haskell in the future.
As an aside using NFData is also very important in parallel programming to ensure that operations are done on specific threads and not on a thread that is later viewing their results. If you are not very careful then you be sticking thunks representing large amounts of work into an MVar or Chan and then having a common thread evaluate them in future which is usually not the desired behavior.
The implementation of safeCatch is very simple:
So now if we provided an instance of NFData for our MyRecord type we could do the following:
Now any exceptions that occur when decoding will be caught by the exception handler. Pretty cool.
Now the second technique I use can be used by itself or in conjunction with what I have just showed. I often do the latter. Developers will frequently find themselves assign data collected from the outside world into records. Since the assignment of fields in a record will be lazy even though the record itself may be evaluated (weak head normal form) we may want to create an exception handler on the field assignment. Fortunately there is a great function called mapException which has two important properties. First the return value it pure which we need for field assignment. Second it takes a function which allows us to transform one exception into a new one. Developers with experience in Java or C# will no doubt be familiar with inner exceptions and this allows us to do the same thing. Under the hood mapException uses unsafePerformIO to work its magic. The comments note that this is proven safe in the paper.
While there is a lot more to know about exceptions I hope this proves useful to some of you out there.