Thoughts on CallStack and withFrozenCallStack

I've been adding CallStack to Cabal's source code, and I noticed a common pattern that doesn't seem to be supported by the GHC.Stack API. Imagine I'm writing a general purpose logging function: debug :: Verbosity -> String -> IO () I'd like this function to print out the SrcLoc of the person who called; as an additional requirement, I only want to print the top-most stack frame by default (someone can ask for the full stack if they really want to). If I look at callStack, it is going to contain a stack frame for debug itself. That's not so great; the obvious way to fix this is to pop off the irrelevant frame manually. But now suppose that I have a pprDebug which itself calls debug. I don't want pprDebug to show up in the stack frame! Declaratively, I want to say, "Please don't add me to the call stack", at the function *definition*, not the call-site (which is what withFrozenCallStack) gives me. Does this seem like a reasonable request? I have several other ways to do this: - You could just use withFrozenCallStack at the definition site. Then when pprDebug calls debug, debug will get the call stack that pprDebug had; thus, there will always only be one irrelevant frame to pop off. - One problem with using withFrozenCallStack is that you *do* lose useful trace information, which you kind of might actually want if you are being verbose. So it almost seems like, the frozen API should still *push* frames, but just not make them visible by default. - Something that does NOT work is having functions edit the call stack to remove themselves. There's no way to tell if the stack is frozen or not, and thus no way to tell if you are actually on the stack (and need to remove it.) Thanks, Edward

On Wed, Sep 7, 2016, at 15:10, Edward Z. Yang wrote:
I've been adding CallStack to Cabal's source code, and I noticed a common pattern that doesn't seem to be supported by the GHC.Stack API.
Imagine I'm writing a general purpose logging function:
debug :: Verbosity -> String -> IO ()
I'd like this function to print out the SrcLoc of the person who called; as an additional requirement, I only want to print the top-most stack frame by default (someone can ask for the full stack if they really want to).
If I look at callStack, it is going to contain a stack frame for debug itself. That's not so great; the obvious way to fix this is to pop off the irrelevant frame manually.
Inside debug, the top element of the callStack should be the call-site of debug, which should be exactly what you want. (NB this is new behavior as of GHC 8.0.1, in GHC 7.10.2 you would get an extra frame for the occurrence of ?callStack)
But now suppose that I have a pprDebug which itself calls debug. I don't want pprDebug to show up in the stack frame!
Declaratively, I want to say, "Please don't add me to the call stack", at the function *definition*, not the call-site (which is what withFrozenCallStack) gives me. Does this seem like a reasonable request?
Indeed, this sounds useful. The approach I recently took with adding CallStacks to logs was to have the internal functions (eg formatting and actually printing the logs) take an *explicit* CallStack, and have the external functions take an *implicit* CallStack. Thus the internal functions are excluded from the stack. But this does give us slightly less reuse as, in your example, you couldn't implement pprDebug in terms of debug if you also want to export debug.
I have several other ways to do this:
- You could just use withFrozenCallStack at the definition site. Then when pprDebug calls debug, debug will get the call stack that pprDebug had; thus, there will always only be one irrelevant frame to pop off.
- One problem with using withFrozenCallStack is that you *do* lose useful trace information, which you kind of might actually want if you are being verbose. So it almost seems like, the frozen API should still *push* frames, but just not make them visible by default.
Good point! Another way this could be useful is if you happen to have a frozen CallStack already in-scope at the call-site of debug/pprDebug. In that scenario, the call-site of the logging function would be lost, and you'd end up printing whatever location was at the top of the frozen stack, which would be quite confusing! Thanks for the comments! Eric

So, I also experimented with setting up this type alias in our Prelude: type IO a = HasCallStack => Prelude.IO a It hasn't caused many type checking errors, but here are some observations: - It can cause a lot of redundant constraint warnings. Often with good reason (sometimes, it indicates somewhere that could use the call stack profitably, but isn't at the moment.) In cases where it doesn't, I've been suppressing the message with "_ = callStack", which works well enough :) - When interoperating with functions like 'bracket' which take an IO action as an argument, I often need to take the type "a -> HasCallStack => Prelude.IO b" into "a -> Prelude.IO b" (so that I can use the function defined in base.) This function lets me do the conversion: withLexicalCallStack :: (a -> WithCallStack (IO b)) -> WithCallStack (a -> IO b) withLexicalCallStack f = let stk = ?callStack in \x -> let ?callStack = stk in f x I'm obviously happy to bikeshed names. - I don't think adding implicit parameters in this way can ever cause us to lose tail-call optimization, but the CallStack might grow really big. I guess there isn't any pruning done? Essentially, you have to be very careful about IO actions which call themselves in loops. Also, here are some responses to Eric's message: Excerpts from Eric Seidel's message of 2016-09-07 18:14:16 -0700:
Inside debug, the top element of the callStack should be the call-site of debug, which should be exactly what you want. (NB this is new behavior as of GHC 8.0.1, in GHC 7.10.2 you would get an extra frame for the occurrence of ?callStack)
Yes, you are right!
Indeed, this sounds useful. The approach I recently took with adding CallStacks to logs was to have the internal functions (eg formatting and actually printing the logs) take an *explicit* CallStack, and have the external functions take an *implicit* CallStack. Thus the internal functions are excluded from the stack. But this does give us slightly less reuse as, in your example, you couldn't implement pprDebug in terms of debug if you also want to export debug.
Yes, that workaround does work, but I don't like it!
Good point! Another way this could be useful is if you happen to have a frozen CallStack already in-scope at the call-site of debug/pprDebug. In that scenario, the call-site of the logging function would be lost, and you'd end up printing whatever location was at the top of the frozen stack, which would be quite confusing!
Precisely.
participants (2)
-
Edward Z. Yang
-
Eric Seidel