62

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

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

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

4

6 回答 6

56

trace是最容易使用的调试方法。这不IO完全是因为您指出的原因:无需在IOmonad 中提升您的代码。它是这样实现的

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

所以在幕后有 IO,但unsafePerformIO被用来逃避它。这是一个可能会破坏引用透明度的函数,您可以通过查看它的类型IO a -> a和名称来猜测它。

于 2010-08-23T10:35:32.873 回答
17

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

然而,您仍可以自行承担一些风险IO a -> a,即执行不纯的 IO。这是一个 hack,当然会“遭受”惰性评估,但这就是 trace 只是为了调试而做的事情。

尽管如此,您可能应该采用其他方式进行调试:

  1. 减少调试中间值的需要

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

  3. 使用通用单子。但是,如果您的代码是单子的,请独立于具体的单子编写它。使用type M a = ...代替普通的IO .... 之后,您可以通过转换器轻松组合 monad,并在其上放置一个调试 monad。即使不再需要 monad,您也可以插入Identity a纯值。

于 2010-08-23T10:29:04.037 回答
14

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

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

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

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

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

通常也不需要将所有内容都放入 monad,只需将需要写入“log”值的函数放入 - 例如,您可以只考虑可能需要进行日志记录的子表达式,而保留主要逻辑,然后通过将纯函数和日志计算以通常的方式与fmaps 和诸如此类的组合来重新组合整个计算。请记住,这Writer对于 monad 来说是一种抱歉的借口:无法日志中读取,只能写入,每次计算在逻辑上都独立于其上下文,这使得处理事情变得更容易。

但在某些情况下,即使这样也太过分了——对于许多纯函数,只需将子表达式移动到顶层并在 REPL 中尝试就可以很好地工作。

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

于 2010-08-23T14:49:38.340 回答
2

trace也倾向于高估其对印刷的论据,在此过程中失去了很多懒惰的好处。

于 2010-08-23T15:46:28.140 回答
0

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

于 2010-10-23T12:35:03.263 回答
-4

好吧,由于整个 Haskell 是围绕惰性求值原理构建的(因此计算顺序实际上是非确定性的),因此使用 printf 没有什么意义。

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

于 2010-08-23T10:19:58.117 回答