如何用printfs“调试”Haskell?

问题描述 投票:60回答:6

来自Ocaml社区,我正在尝试学习一些Haskell。过渡进展顺利,但我对调试有点困惑。我曾经在我的ocaml代码中放置(很多)“printf”,检查一些中间值,或者作为标志来查看计算完全失败的位置。

由于printf是一个IO动作,我是否必须解除IO monad中的所有haskell代码才能进行这种调试?或者有更好的方法来做到这一点(如果可以避免,我真的不想手工完成)

我也找到了跟踪功能:http://www.haskell.org/haskellwiki/Debugging#Printf_and_friends,这看起来正是我想要的,但我不明白它的类型:任何地方都没有IO!有人能解释一下跟踪功能的行为吗?

debugging haskell trace printf-debugging
6个回答
54
投票

trace是最容易使用的调试方法。它完全不是因为你指出的原因而不是IO:不需要在IO monad中解除你的代码。它是这样实现的

trace :: String -> a -> a
trace string expr = unsafePerformIO $ do
    putTraceMsg string
    return expr

所以在幕后有IO,但unsafePerformIO用来摆脱它。这是一个可能会破坏参考透明度的函数,您可以猜测它的类型IO a -> a及其名称。


17
投票

trace简直是不纯洁的。 IO monad的要点是保持纯度(类型系统没有注意到IO)并定义语句的执行顺序,否则通过惰性求值实际上不会定义。

然而,根据自己的风险,你可以将一些IO a -> a组合在一起,即执行不纯的IO。这是一个黑客,当然“受到懒惰评估”的影响,但这就是跟踪只是为了调试而做的事情。

不过,您可能应该采用其他方式进行调试:

  1. 减少调试中间值的需要 编写小型,可重用,清晰,通用的函数,其正确性是显而易见的。 将正确的碎片组合成更正确的碎片。 写tests或交互式尝试。
  2. 使用断点等(基于编译器的调试)
  3. 使用通用monad。如果你的代码是monadic,那么把它写成独立于具体的monad。使用type M a = ...而不是普通的IO ...。之后你可以通过变换器轻松组合monad并在它上面放置一个调试monad。即使monad的需求消失了,你也可以插入Identity a作为纯值。

14
投票

对于它的价值,这里有两种“调试”问题:

  • 将中间值(例如特定子表达式的值)记录到递归函数中
  • 检查表达式求值的运行时行为

在严格的命令式语言中,这些通常是一致的。在Haskell中,他们通常不会:

  • 记录中间值可以改变运行时行为,例如通过强制评估否则将被丢弃的术语。
  • 由于懒惰和共享的子表达式,实际的计算过程可能与表达式的表观结构有很大不同。

如果你只想保留中间值的日志,有很多方法可以做到这一点 - 例如,不是将所有内容都提升到IO,一个简单的Writer monad就足够了,这相当于让函数返回2元组它们的实际结果和累加器值(通常是某种列表)。

通常也不需要将所有内容放入monad中,只需要写入“log”值的函数 - 例如,您可以只分解可能需要进行日志记录的子表达式,使主逻辑保持纯净,然后通过fmaps和诸如此类的常规方式将纯函数和记录计算相结合来重新组装整体计算。请记住,Writer是monad的一个令人遗憾的借口:无法从日志中读取,只写入它,每个计算在逻辑上独立于其上下文,这使得更容易处理周围的事情。

但在某些情况下,即使是过度杀戮 - 对于许多纯函数来说,只需将子表达式移动到顶层并在REPL中尝试一下就可以了。

但是,如果你想实际检查纯代码的运行时行为 - 例如,找出子表达式偏离的原因 - 通常没有办法从其他纯代码中做到这一点 - 事实上,这实质上是纯度的定义。因此,在这种情况下,您别无选择,只能使用纯语言“外部”存在的工具:或者使用诸如unsafePerformPrintfDebugging之类的函数 - errr,我的意思是trace - 或者修改后的运行时环境,例如GHCi调试器。


2
投票

trace也倾向于过度评估其打印论点,在此过程中失去了许多懒惰的好处。


0
投票

如果您可以在研究输出之前等到程序完成,那么堆叠Writer monad是实现记录器的经典方法。我使用这个here从不纯的HDBC代码返回结果集。


-4
投票

好吧,因为整个Haskell是围绕惰性求值的原则构建的(因此计算的顺序实际上是非确定性的),所以使用printf对它没有多大意义。

如果REPL +检查结果值对于您的调试来说真的不够,那么将所有内容包装到IO中是唯一的选择(但它不是Haskell编程的正确方法)。

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