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"

【问题讨论】:

发生这种情况(我猜)是因为 ophttpCall 不是函数,所以 GHC 必须决定“调用点”在哪里,“调用”登录到堆栈跟踪。这是通过类型推断解决约束的地方,这可能不是那么直观。考虑通过httpCall :: HasCallStack => () -> IO () 来消除歧义,只是为了试验,看看结果是否更直观。 如果this 是你的意思,它不会改变任何东西。 上面的代码是gist(我无法冻结repl.it代码) 【参考方案1】:

对于问题 1 和 2,请注意 HasCallStack 调用堆栈基本上是“词法”的。

具体来说,关于“问题1”,调用堆栈中的调用位置(函数名和行号)信息是基于被调用函数在程序中的语法位置,而不是实际强制调用的代码行打电话。所以,httpCall 被显示是从第 17 行而不是第 14 行调用的原因是因为文字程序文本“httpCall”出现在第 17 行。类似地,对于“问题 2”,第 14 行调用的东西出现在词法上作为op,而不是httpCall,所以这就是它在调用堆栈中的显示方式。

关于你的程序的行为:

在您的第一个程序中,mainretryhttpCallHasCallStack 约束通过将适当的调用条目(带有词法调用站点信息)推送到空调用堆栈来解决(因为 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) 的 retrycallStack 参数实际上并未被使用。当httpCall 被评估时,它会通过push2 emptyCallStack,这会为main 中的httpCall 调用生成一个条目。

我认为所有这些或多或少都是您假设发生的事情(即,HasCallStack 约束在 main 中得到解决以适应 retry 的参数类型)。

在您的第二个程序中,发生了一些可能没有很好记录的有趣事情。 retry 调用的处理方式与以前相同,但 httpCall - 因为它的类型为 HasCallStack => IO () 而不是 IO () - 生成期望实例在范围内而不是解析实例的代码立即地。使用显式调用堆栈参数,为 retrymain 生成的代码现在看起来像:

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 作为其参数,因此您可以看到httpCallopretry 的三个条目按顺序排列。

【讨论】:

以上是关于Haskell HasCallStack 意外行为的主要内容,如果未能解决你的问题,请参考以下文章

WindowsLookAndFeel关于按钮着色的意外行为

Swift中的嵌套函数意外行为?

UIlabel 大小以适应意外行为

连接字符串的意外行为

NSFetchRequest 的意外行为

方法重载解决意外行为