如何使用printf在Haskell中进行“调试”?

63

我来自Ocaml社区,现在正在学习Haskell。转换进展顺利,但在调试方面有些困惑。我曾经在我的Ocaml代码中放置(很多)"printf"语句以检查一些中间值,或者作为标志来确定计算到哪里失败。

由于printf是一个IO操作,我是否需要将所有的Haskell代码提升到IO单子内,才能进行这种调试?或者有更好的方法来做到这一点(如果可以避免手工操作,我真的不想这样做)

我还发现了trace函数: http://www.haskell.org/haskellwiki/Debugging#Printf_and_friends 它似乎正是我想要的,但我不理解它的类型:没有任何的IO! 有人能解释一下trace函数的行为吗?


6
需要注意的是,trace仅用于调试,如果您将其用于“真正”的逻辑,社区将会避开它。 - luqui
6个回答

57

trace是最容易使用的调试方法。它不是在IO中实现的,正如您所指出的那样:没有必要将您的代码提升到IO单子中。它的实现方式如下:

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

因此,在幕后有IO操作,但是使用unsafePerformIO来跳出它。这是一个可能会破坏引用透明性的函数,你可以从它的类型IO a -> a和名称看出来。


18

trace 只是简单地使其不纯。 IO monad 的重点是保持纯度(没有被类型系统忽略的 IO)并定义语句的执行顺序,否则通过惰性评估实际上将是未定义的。

然而,你仍然可以自己冒险编写一些 IO a -> a,即执行不纯的 IO。这是一个 hack,当然会 "遭受" 惰性评估的影响,但出于调试的目的,这就是 trace 所做的事情。

尽管如此,你应该考虑其他调试方法:

  1. 减少对中间值的调试需求

    • 编写小型、可重用、清晰、通用的函数,其正确性显而易见。
    • 组合正确的部分以形成更大的正确部分。
    • 编写 测试 或交互式地尝试各个部分。
  2. 使用断点等(基于编译器的调试)

  3. 使用通用的 monads。如果你的代码仍然是 monadic 的,那么独立于具体的 monad 编写它。使用 type M a = ... 而不是普通的 IO ...。之后,你可以轻松地通过 transformers 组合 monads 并在其上放置一个调试 monad。即使不再需要 monads,你也可以为纯值插入 Identity a


14

就目前而言,这里实际上有两种“调试”:

  • 记录中间值,例如递归函数的每次调用中特定子表达式的值
  • 检查表达式求值的运行时行为

在严格的命令式语言中,这两者通常是一致的。但在 Haskell 中,它们经常不一致:

  • 记录中间值可能会改变运行时行为,例如通过强制计算本应被抛弃的术语。
  • 由于惰性和共享子表达式,计算实际过程可能与表达式的显式结构截然不同。

如果您只想保留中间值的日志记录,有许多方法可以实现这一点 - 例如,除了将所有内容提升到 IO 外,简单的 Writer 单子足以胜任这项任务,这相当于使函数返回其实际结果和累加器值(通常是某种列表)的 2 元组。

通常也没有必要将 所有 内容都放入单子中,只需将需要写入“日志”值的函数放入其中即可 - 例如,您可以将可能需要记录日志的子表达式分解出来,将主逻辑保持为纯逻辑,然后通过 fmap 等方式将纯函数和记录计算组合起来。请记住,Writer 在某种程度上是一种不太适合用作单子的借口:由于没有从日志中读取值的方法,只能将值写入其中,每个计算都在逻辑上独立于其上下文,这使得处理事物更加容易。

但在某些情况下,即使这样也有点过头了 - 对于许多纯函数来说,仅将子表达式移到顶层并在 REPL 中尝试可能会很有效。

如果你想实际检查纯代码的运行时行为,例如找出子表达式发散的原因,通常情况下是 无法从其他纯代码中进行 ——实际上,这本质上就是“纯度”的定义。因此,在这种情况下,你别无选择,只能使用“外部”存在的工具:要么是不纯的函数(如unsafePerformPrintfDebugging——噢,我的意思是 trace),要么是修改后的运行环境,如 GHCi 调试器。


2

trace 也往往会过度评估其参数以进行打印,从而失去了惰性的许多好处。


那么就不要这样做 :-) (即只跟踪您有信心强制执行的内容) - sclv

0

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


由于惰性求值:您实际上不必等到程序完成。 - Jeremy List

-4

嗯,由于整个Haskell都是围绕惰性求值的原则构建的(因此计算顺序实际上是不确定的),在其中使用printf几乎没有意义。

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


1
由于你的两个说法都是错误的,所以被踩了。在需要调用时,计算顺序是静态确定的。它不仅由函数体确定,还由函数执行的外部上下文确定。这不像数据依赖关系在运行时确定,并根据运行时值不可预测地直接控制流程。至于第二段 - 除了REPL之外还有很多选择:跟踪(包括比“trace”更智能的堆检查器),测试,ghci调试器以及针对其的图形前端(如leksah等)。 - nponeccop

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接