The following is a simplification of a pretty common pattern we have, where you have some retry combinator wrapping an IO operation. I would like to have some stack traces so I added the HasCallStack
constraint but the resulting stacktrace was not really satisfactory:
import Control.Monad (forM_)
import GHC.Stack
httpCall :: HasCallStack => IO ()
httpCall = do
putStrLn $ prettyCallStack callStack
print "http resolved"
retry :: HasCallStack => IO () -> IO ()
retry op =
forM_ [1] $ \i ->
op
main :: IO ()
main = retry httpCall
stacktrace:
CallStack (from HasCallStack):
httpCall, called at main.hs:16:14 in main:Main
"http resolved"
I assumed the HasCallStack
constraint gets resolved in main
to fit the argument type of retry
so I added the constraint to the argument type:
{-# LANGUAGE RankNTypes #-}
import Control.Monad (forM_)
import GHC.Stack
httpCall :: HasCallStack => IO ()
httpCall = do
putStrLn $ prettyCallStack callStack
print "http resolved"
retry :: HasCallStack => (HasCallStack => IO()) -> IO () -- NOTICE the constraint in the argument
retry op =
forM_ [1] $ \i ->
op
main :: IO ()
main = retry httpCall
Now the stacktrace has 2 more entries both of them quite surprising:
CallStack (from HasCallStack):
httpCall, called at main.hs:17:14 in main:Main
op, called at main.hs:14:5 in main:Main
retry, called at main.hs:17:8 in main:Main
"http resolved"
httpCall
reports it was called from main
(line 17)op
reports the correct line but is quite unexpected to see it in the stacktrace to begin with.I expected something along the lines of:
CallStack (from HasCallStack):
httpCall, called at main.hs:14:5 in main:Main
retry, called at main.hs:17:8 in main:Main
"http resolved"
For both problems 1 and 2, note that HasCallStack
call stacks are fundamentally "lexical".
Specifically, with respect to "problem 1", the call site (function name and line number) information in the call stack is based on where syntactically in the program the called function appears, not the line of code that actually forces the call to be made. So, the reason httpCall
is shown to be called from line 17 and not line 14 is because the literal program text "httpCall
" appears on line 17. Similarly for "problem 2", the thing called on line 14 appears lexically as op
, not httpCall
, so that's how it appears in the call stack.
With respect to the behavior of your programs:
In your first program, the HasCallStack
constraints on retry
and httpCall
in main
are resolved by pushing appropriate call entries (with lexical call site information) onto an empty call stack (because main
provides no HasCallStack
instance itself). If we made the call stack argument explicit, the code would look something like:
httpCall :: CallStack -> IO ()
httpCall callStack = do
putStrLn $ prettyCallStack callStack
print "http resolved"
retry :: CallStack -> IO () -> IO ()
retry callStack op =
forM_ [1] $ \i ->
op
main :: IO ()
main = retry (push1 emptyCallStack) (httpCall (push2 emptyCallStack))
where push1 = pushCallStack ("retry", SrcLoc "" "" "" 15 7 15 11)
push2 = pushCallStack ("httpCall", SrcLoc "" "" "" 15 13 15 20)
Note that the callStack
argument to retry
that records the retry
call (push1
) isn't actually used. When httpCall
is evaluated, it gets passed push2 emptyCallStack
which generates a single entry for the httpCall
call in main
.
I think all that is more or less what you assumed was happening (i.e., the HasCallStack
constraint gets resolved in main to fit the argument type of retry
).
In your second program, something interesting happens that perhaps isn't well documented. The retry
call is treated the same way as before, but the httpCall
-- because it is typed as HasCallStack => IO ()
instead of IO ()
-- produces code that expects an instance to be in scope instead of resolving the instance immediately. With explicit call stack arguments, the code generated for retry
and main
would now look something like:
retry :: CallStack -> (CallStack -> IO ()) -> IO ()
retry callStack op =
forM_ [1] $ \i ->
op (push3 callStack)
where push3 = pushCallStack ("op", SrcLoc "" "" "" 14 4 14 6)
main :: IO ()
main = retry (push1 emptyCallStack) (\callStack -> httpCall (push2 callStack))
where push1 = pushCallStack ("retry", SrcLoc "" "" "" 17 7 17 11)
push2 = pushCallStack ("httpCall", SrcLoc "" "" "" 17 13 17 20)
The result is that httpCall
actually gets push2 $ push3 $ push1 emptyCallStack
as its argument, so you see the three entries for httpCall
, op
, and retry
in that order.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With