Monadic stack traces that make a lot of sense

Call traces are a recurrent topic in the Haskell community. Nearly every programming language out there has this feature, except Haskell. For an imperative language providing stack traces is easy, since the runtime stack contains all the information needed, and it can be retrieved essentially for free. But in a lazy language the stack does not contain call frames, and hence providing a “stack trace” is a much more involved task. As existing efforts we can cite the -xc flag of the GHC profiling runtime, which provides a stack trace based on cost centers; and the GHCi debugger :trace command which records a lazy call trace. There is also the Hat tool which is capable of providing a post-mortem stack trace via a program transformation, but I don’t know if you can make Hat work nowadays. And the not yet released tool for GHC presented by Tristan Allwood in the Haskell Symposium(video). With exception of the latter, which is not yet available, I believe that all these options are not really solving the lack of stack traces in practice.

While working on the control-monad-exception and by suggestion of Jeff Heard I have been looking at providing exception call traces. That is, making a call trace available to an exception handler. This can be very useful to give more detailed error messages which help to diagnose a problem in our code. For instance, recall the example in my previous post.

data ProcessError = NotThreeLines String
   deriving (Show, Typeable) 
instance E.Exception ProcessError

process :: FilePath -> AttemptT IO Int 
process filePath = do
       contents <- A.readFile filePath
       case lines contents of
           [num1S, opS, num2S] -> do
               num1 <- num1S
               op   <- opS
               num2 <- num2S
               return $ toFunc op num1 num2
           _ -> Failure $ NotThreeLines contents is a custom version of read which throws an exception if fails to read its argument. There are three different source code locations in which may fail in the body of process. Getting a ReadFail exception is not going to tell you *which* read attempt failed, in the same way that a “head of empty list” error does not tell you which call to head of all the calls to head in your program failed. With support for exception call traces, you could get an error message for this exception that looks like this:

error: ReadFail: could not read the String ....
  in Main.hs: line 27, col 20
  in ...
  in ...
  in Process.hs: line 12, col 15

This is not possible right now. None of the libraries for exceptions in Hackage support this kind of call traces. Admittedly, the situation is not as bad as the “head of empty list” case, because if you are using a version of head that throws an exception, chances are high that the exception will be captured close enough to the call site that it is clear where the error is coming from. But still, anything below this level of error reporting is substandard in a modern programming language.

In this post I show how a standard Error monad can be extended to carry call traces. An error monad in Haskell can be implemented by the Either datatype, a suitable Monad instance, and a pair of throw and catch functions. The Left constructor denotes an exception, defined here as the SomeException type in ControlException, and the Right constructor denotes a succesful computation. Omitting unessential cruft the code looks like:

type ErrorM a = Either SomeException a
throw :: e -> ErrorM a 
throw = Left 

instance Monad (Either e) where
   return = Right
   Left  e >>= _ = Left e
   Right v >>= f = f v 

catch :: Exception e => ErrorM a -> (e -> ErrorM a) -> ErrorM a
catch (Right v) _ = Right v 
catch (Left e) h = ...

Nothing surprising here. I omitted part of the implementation of catch because it is slightly more involved due to the existential machinery that makes SomeException work.

We want to extend this interface with a function catchWithCallTrace which provides access to the call trace. Something with a signature

catchWithCallTrace :: Exception e => ErrorM a -> (e -> [SrcLoc] -> ErrorM a) -> ErrorM a
type SrcLoc = String

where SrcLoc is some abstract datatype for source code locations. In this case a simple String suffices.
To generate this call trace, the first step is to extend our ErrorM monad to keep a list of source code locations in the Left constructor.

type ErrorM a = Either (SomeException, [SrcLoc]) a

The next step is modify the definition of bind so that it inserts the current source code location in the call trace whenever it spots an exception.

Left (e, trace) >>= f = Left (e, <currentloc> : trace)

In this way whenever the exception reaches a handler, it will find a list of SrcLocs starting from the throw site all the way up to the handler site stored in the computation. This constitutes a monadic call trace, which due to the order of evaluation imposed by the Either monad, resembles very closely a stack trace from an imperative language. This is great, because that is exactly the kind of call trace that makes sense to the programmer. I have been testing the library with a small personal project involving a CGI applet and a database, and the capture below shows the kind of error output that my CGI applet provides.

The implementation of catchWithCallTrace now is straightforward.

catchWithCallTrace (Left (exception, trace)) h = h exception trace

The above line is pseudocode since it omits the handling of the existential stuff for SomeException, but it conveys the idea.
All what is missing now is to turn the equation for (Left e >>= f) above into real Haskell. Those source code locations must be made available to (>>=). This is not possible in the standard definition of (>>=), so we extend the Monad type class with a bindWithSrcLoc method, polluting its categorical elegance with the concerns of mundane coders.

class Monad where
   bindWithSrcLoc :: SrcLoc -> m a -> (a -> m b) -> m b
   bindWithSrcLoc _ = (>>=)

This is a conservative extension which breaks no existing code and has no performance penalty. There is a default implementation which ignores the source location and calls bind, and there should be no performance penalty at all for using bindWithSrcLoc in a regular monad which does nothing with src locs, if we can trust GHC to inline the body of (>>=) in the default definition.

For the ErrorM monad we give bindWithSrcLoc a different implementation:

instance Monad ErrorM where
   bindWithSrcLoc srcloc (Left (e, trace)) _ = Left (e, srcloc:trace) 
   bindWithSrcLoc srcloc (Right x) f = f x

That is all what is needed to provide monadic call traces. In order to make programming with bindWithSrcLoc feasible, Haskell compilers can desugar do-notation using the bindWithSrcLoc variant, providing accurate source code locations which are available at “desugaring time”. Yes, this means that do-notation stops being mere syntactic sugar to become something more, but I perhaps it is a small price to pay if we gain monadic call traces in the process? Once there is support for this in the compiler, any monad can implement this interface and provide this facility, including the IO monad, the ErrorT monad transformer from the mtl package, and any other error handling monad.

To wrap up this post, I am making available an experimental implementation of monadic call traces with the 0.5 release of control-monad-exception. Obviously I can’t just extend the Monad class in a package, so the mechanism used in this release is not as elegant as the one presented here. It uses a MonadLoc class to deal with source code locations and includes the MonadLoc preprocessor, a tool based on haskell-src-exts (thanks Niklas for such an awesome library, the entire code for the preprocessor fits in a few lines of haskell) which makes the source code locations happen in the right places. When, if ever, the Monad class is extended to provide source locations, the MonadLoc preprocessor will no longer be needed. But in order to get monadic stack traces now, all you need to do is to use the Control.Monad.Exception.EMT monad in you code and insert the following pragma at the top of your files:

{-# OPTIONS_GHC -F -pgmF MonadLoc #-}

I should also mention that nothing in the monadloc package is specific to control-monad-exception. Any other monad can implement the MonadLoc type class and benefit from the MonadLoc preprocessor to provide monadic call traces.

Enjoy your Haskell monadic call traces!

2 thoughts on “Monadic stack traces that make a lot of sense

  1. sorry but Hackage is down, so the version of control-monad-exception with the MonadLoc preprocessor is not available yet. It will be as soon as Hackage is back !

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s