使用Haskell异常捕获调用堆栈

2021-01-23 05:54:09

最近,我发现了一种几乎透明地捕获Haskell异常中的调用堆栈的好方法,我将在本文中与大家分享

foo :: [a]-> foo [] =错误"不可能!" foo a:_ =条形:: :: [Int]-> [Int]->整数条a b = foo a + foo b

λ> bar [] [] ***例外:不可能! CallStack(来自HasCallStack):错误,在堆栈中调用。hs:4:10 in main:Main

显然,这是一个错误,但是GHCI也打印出一个奇特的东西:调用堆栈!它仅包含一个条目,但是对您没有帮助。因此,您可以转到GHC手册,查看消息在谈论什么HasCallStack,它位于:HasCallStack部分。

foo :: HasCallStack => [a]-> foo [] =错误"不可能!" foo a:_ =条形:: HasCallStack => [Int]-> [Int]->整数条a b = foo a + foo b

λ> bar [] [] ***例外:不可能! CallStack(来自HasCallStack):错误,在堆栈上调用.hs:6:10在main:主foo中,在堆栈上调用.hs:10:11在main:主栏,在< interactive&gt ;: 5:1中调用互动式:Ghci1

从ghci提示到调用错误的位置,您可以获得所有调用的函数名称,模块名称甚至源位置。

但是请记住,仅在有HasCallStack约束的情况下,才从错误调用中捕获该堆栈。例如,从foo中删除约束也会从日志中排除bar:

λ> bar [] [] ***例外:不可能! CallStack(来自HasCallStack):错误,在堆栈中调用.hs:6:10 in main:Main

不过,您仍然可以知道错误调用的确切位置,这很好。

注意:头,尾,读等使用errorWithoutStackTrace(出于性能原因),因此您永远不会看到来自它们的堆栈跟踪。避免头的另一原因!

但是,使用错误报告错误不是很方便:您只能传递一个String作为参数,因此在传播其他错误时捕获特定错误变得非常困难和混乱。

幸运的是,GHC中还有另一种机制:异常。因此,您可以定义自定义异常类型,并通过foo函数将其抛出,如下所示:

数据FooException = FooException派生(显示,异常)foo :: HasCallStack => [a]-> a foo [] =抛出FooException foo a:_ =条形:: HasCallStack => [Int]-> [Int]->整数条a b = foo a + foo b

您运行它并期望看到带有堆栈跟踪的漂亮异常。但…

原因是,即使从具有HasCallStack上下文的地方抛出异常,异常也不会自动捕获堆栈跟踪。这样做有一个未解决的问题,早在2016年就报告过,但尚未取得任何进展。

但是,如果我们想捕获异常堆栈呢?一种可能的方法是将堆栈(表示为CallStack类型)保存为异常构造函数的一部分,然后进行自定义throwWithStack :: HasCallStack =>。 Foo-> IO()函数可以在任何地方使用它,但这太麻烦了,您可能只是忘记使用正确的throwing函数。

幸运的是,有更好的方法。回想一下神奇的HasCallStack约束从使用注释的点捕获调用栈。我们不想注释异常,但是同一行还有另外一件事-异常构造函数本身!事实证明,您可以使用GADT捕获具有异常数据的堆栈:

实例Show FooException,其中show FooException =" FooException \ n" <> prettyCallStack callStack派生任何类实例Exception FooException-或者,从股票派生Show并在' displayException'中打印调用堆栈。方法。

这里的callStack由GHC.Stack提供,将使用在FooException GADT构造函数上由模式匹配引入的HasCallStack约束。

λ> bar [] [] ***异常:FooException CallStack(来自HasCallStack):FooException,在堆栈处调用。hs:18:16 in main:Main foo,在堆栈处调用。hs:22:11 in main:Main bar,称为在< interactive>中:7:1在inactive:Ghci1中

再举一个例子,这是我在生产代码中复制的真实调用堆栈:

例外:操作超时CallStack(来自HasCallStack):超时,在src / Database / Bolt / Connection.hs处调用:hasbolt-0.1中的38:36。 4.3 -inplace:Database.Bolt.Connection运行,在xxx-0.3中以src / XXX / DB / Impl.hs:42:43调用。 5.0 -inplace:XXX.DB.Impl runDB,在src / XXX / DB / Impl.hs处调用:xxx-0.3中的124:14。 5.0 -inplace:XXX.DB.Impl程序,在src / XXX / API / Program.hs中调用:xxx-0.3中的33:17。 5.0 -inplace:XXX.API.Program

HasCallStack是一个魔术约束,因此此技巧起作用的事实可能是偶然的,也可能不是偶然的:GHC的某些后续更改可能会阻止GADT模式匹配影响HasCallStack的求解方式。但是,我认为这种方法足够有用,可以在实践中使用。只是不要忘记在可能出现故障的地方添加足够的HasCallStack。

不过,请不要忘记,HasCallStack不是免费的,有时会破坏某些优化,特别是在递归函数中使用时(这就是head& amp; friends不会捕获堆栈的原因)。

当然,这篇文章对调试IOError等标准异常没有任何帮助。为此,通常的方法是使用-prof进行构建,并使用+ RTS -xc运行代码,如手册中所述。