Haskell HasCallStack意外行为

问题描述 投票:3回答:1

以下是我们拥有的一个非常普通的模式的简化,在这里您可以使用一些重试组合器来包装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

stacktrace:

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

现在stacktrace还有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"

问题

  1. [httpCall报告它是从main调用的(第17行)
  2. [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"
haskell ghc stack-trace
1个回答
0
投票

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

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

关于程序的行为:

[在您的第一个程序中,HasCallStackretry中的httpCallmain约束通过将适当的调用条目(带有词法调用站点信息)推送到空调用堆栈中来解决(因为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)

请注意,callStackretry自变量实际上记录了retry调用(push1)。评估httpCall时,将传递push2 emptyCallStack,它会为httpCall中的main调用生成单个条目。

我认为您所假设的一切或多或少都发生了(即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的三个条目。

© www.soinside.com 2019 - 2024. All rights reserved.