@hackage require-callstack0.2.0.0

Propagate HasCallStack with constraints

require-callstack

Haskell has opt-in call stacks through the use of the HasCallStack constraint. One unfortunate aspect of this design is that the resulting CallStack can be truncated if any function in the call list omits the constraint.

foo :: HasCallStack => Int -> String
foo = error "oh no"

bar :: HasCallStack => Int -> String
bar = foo . negate

baz :: Int -> String
baz = bar . (* 2)

main :: IO ()
main = do
    print $ baz 5

Running this code will fail with an ErrorCall "oh no" exception. The attached CallStack will only mention foo and bar - baz will not be present, nor will main. A truncated CallStack isn't nearly as useful as you might like.

One solution is the annotated-exception library, which can attach CallStack to any thrown exception, and catch is guaranteed to add a stack frame at the catch-site for any exception that passes through. However, it's still nice to have HasCallStack entries on functions - then you get the name of the function, which makes diagnosing an error report easier.

This library introduces a type RequireCallStack. Unlike HasCallStack, this isn't automagically solved - if you call a function that has RequireCallStack in the constraint, you must either call provideCallStack to discharge the constraint, or add RequireCallStack to the signature of the function you're defining.

panic :: RequireCallStack => String -> a
panic = error

foo :: RequireCallStack => Int -> String
foo = panic "oh no"

bar :: RequireCallStack => Int -> String
bar = foo . negate

baz :: Int -> String
baz = bar . (* 2)

main :: IO ()
main = do
    print $ baz 5

This code will fail with a compile-time error:

/home/matt/Projects/require-callstack/test/Main.hs:30:5: error: [GHC-39999]
    • No instance for ‘RequireCallStack.Internal.Add_RequireCallStack_ToFunctionContext_OrUse_provideCallStack’
        arising from a use of ‘bar’
        ....
   |        
30 |     bar . (* 2)
   |     ^^^

The error message, read carefully, will tell you how to solve the issue. If we then write:

panic :: RequireCallStack => String -> a
panic = error

foo :: RequireCallStack => Int -> String
foo = panic "oh no"

bar :: RequireCallStack => Int -> String
bar = foo . negate

baz :: RequireCallStack => Int -> String
baz = bar . (* 2)

main :: IO ()
main = provideCallStack $ do
    print $ baz 5

Then the code compiles and works as expected.