Haskell HasCallStack 意外行为
Posted
技术标签:
【中文标题】Haskell HasCallStack 意外行为【英文标题】:Haskell HasCallStack unexpected behavior 【发布时间】:2020-03-07 03:25:17 【问题描述】:以下是我们非常常见的模式的简化,其中您有一些重试组合器包装 IO 操作。我想要一些堆栈跟踪,所以我添加了HasCallStack
约束,但生成的堆栈跟踪并不令人满意:
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
堆栈跟踪:
CallStack (from HasCallStack):
httpCall, called at main.hs:16:14 in main:Main
"http resolved"
我假设 HasCallStack
约束在 main
中得到解析以适应 retry
的参数类型,所以我将约束添加到参数类型:
-# 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
现在堆栈跟踪还有 2 个条目,这两个条目都非常令人惊讶:
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
报告它是从 main
调用的(第 17 行)
op
报告了正确的行,但从一开始就在堆栈跟踪中看到它是非常出乎意料的。
我预计会出现以下情况:
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"
【问题讨论】:
发生这种情况(我猜)是因为op
和 httpCall
不是函数,所以 GHC 必须决定“调用点”在哪里,“调用”登录到堆栈跟踪。这是通过类型推断解决约束的地方,这可能不是那么直观。考虑通过httpCall :: HasCallStack => () -> IO ()
来消除歧义,只是为了试验,看看结果是否更直观。
如果this 是你的意思,它不会改变任何东西。
上面的代码是gist(我无法冻结repl.it代码)
【参考方案1】:
对于问题 1 和 2,请注意 HasCallStack
调用堆栈基本上是“词法”的。
具体来说,关于“问题1”,调用堆栈中的调用位置(函数名和行号)信息是基于被调用函数在程序中的语法位置,而不是实际强制调用的代码行打电话。所以,httpCall
被显示是从第 17 行而不是第 14 行调用的原因是因为文字程序文本“httpCall
”出现在第 17 行。类似地,对于“问题 2”,第 14 行调用的东西出现在词法上作为op
,而不是httpCall
,所以这就是它在调用堆栈中的显示方式。
关于你的程序的行为:
在您的第一个程序中,main
中 retry
和 httpCall
的 HasCallStack
约束通过将适当的调用条目(带有词法调用站点信息)推送到空调用堆栈来解决(因为 main
提供没有HasCallStack
实例本身)。如果我们明确调用堆栈参数,代码将如下所示:
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)
请注意,记录 retry
调用 (push1
) 的 retry
的 callStack
参数实际上并未被使用。当httpCall
被评估时,它会通过push2 emptyCallStack
,这会为main
中的httpCall
调用生成一个条目。
我认为所有这些或多或少都是您假设发生的事情(即,HasCallStack
约束在 main 中得到解决以适应 retry
的参数类型)。
在您的第二个程序中,发生了一些可能没有很好记录的有趣事情。 retry
调用的处理方式与以前相同,但 httpCall
- 因为它的类型为 HasCallStack => IO ()
而不是 IO ()
- 生成期望实例在范围内而不是解析实例的代码立即地。使用显式调用堆栈参数,为 retry
和 main
生成的代码现在看起来像:
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)
结果是httpCall
实际上将push2 $ push3 $ push1 emptyCallStack
作为其参数,因此您可以看到httpCall
、op
和retry
的三个条目按顺序排列。
【讨论】:
以上是关于Haskell HasCallStack 意外行为的主要内容,如果未能解决你的问题,请参考以下文章