If g throws an error because its argument is bad, we'd like to know how that happened. A HasCallStack constraint on g will reveal that g was called by f. Now we'd like to dig deeper. Was f supplied a bad argument? Did h calculate a bad value from it? If f is only called a few times, it's easy to work this out: just add a trace call to f reporting x. But if f is called thousands of times, that won't work at all.
f :: Int -> Int
f x = recordInCallStack msg (g (h x))
where
msg = "x = " ++ show x
Now when g throws an error, we'll see the message f recorded in its proper position in the call stack.
== Implementation ==
I believe the cleanest approach would be to add a new constructor to CallStack (perhaps called Message):
Message :: String -> CallStack -> CallStack
Then recordInCallStack can look like
recordInCallStack
:: forall (a :: TYPE rep)
HasCallStack
=> String
-> (HasCallStack => a)
-> a
recordInCallStack s a =
let ?callStack = Message s ?callStack
in a
== Questions ==
For performance reasons, I believe recordInCallStack should be lazy in its string argument. But this means that we could encounter another error in the process of printing a call stack. How if at all should we attempt to recover in this case?
If we don't do anything special, I believe we may start printing a second call stack in the middle of the first. Kind of gross.
Another option is to catch all exceptions when evaluating the message. Then we could note that the message couldn't be printed but continue with the rest of the call stack.
David