如何与printfs“调试” Haskell?
-
30-09-2019 - |
题
来自OCAML社区,我正在努力学习一些Haskell。过渡进展顺利,但我对调试有点困惑。我曾经在OCAML代码中放入(很多)“ printf”,检查一些中间值或作为标志,以查看计算的确切失败。
因为printf是一个 io 动作,我必须将所有Haskell代码放在 io 单调可以进行这种调试?还是有更好的方法来做到这一点(如果可以避免的话,我真的不想手工做)
我也发现 痕迹 功能 :http://www.haskell.org/haskellwiki/debugging#printf_and_friends这似乎正是我想要的,但我不明白它是类型:没有 io 在任何地方!有人可以向我解释跟踪功能的行为吗?
解决方案
trace
是最容易使用调试方法的方法。不在 IO
正是由于您指出的原因:无需在 IO
单子。它是这样实施的
trace :: String -> a -> a
trace string expr = unsafePerformIO $ do
putTraceMsg string
return expr
因此,幕后有IO unsafePerformIO
用于逃脱它。这是一个可能破坏参考透明度的函数,您可以猜测它的类型 IO a -> a
还有它的名字。
其他提示
trace
只是不纯净。这一点 IO
MONAD是要保留纯度(类型系统未注意的IO)并定义了声明的执行顺序,否则该声明实际上会通过懒惰的评估来确定。
但是,出于自身的危险,您仍然可以一起入侵一些 IO a -> a
, ,即执行不纯净的IO。这是一个黑客攻击,当然是懒惰评估的“遭受痛苦”,但这就是Trace仅仅为了调试而做的。
但是,您可能应该采取其他调试方法:
减少对中间值调试的需求
- 写出明显的小,可重复使用,清晰,通用的功能。
- 将正确的碎片结合在一起,以更正确的零件。
- 写 测试 或互动尝试。
使用断点等(基于编译器的调试)
使用通用的单子。但是,如果您的代码仍然是monadic,则将其独立于混凝土单子编写。采用
type M a = ...
而不是普通IO ...
. 。之后,您可以轻松地通过变形金刚结合单调,并在其上放置调试单元。即使需要单调的需要消失,您也可以插入Identity a
对于纯值。
就其价值而言,实际上有两种“调试”。
- 记录中间值,例如特定子表达在每个调用中具有递归函数的值
- 检查表达式评估的运行时行为
在严格的命令式语言中,这些通常是重合的。在Haskell中,他们通常不会:
- 记录中间值可以改变运行时的行为,例如强制对原本会丢弃的术语的评估。
- 由于懒惰和共享亚表达,计算的实际过程可能与表达的明显结构有很大不同。
如果您只想保留中间值的日志,则有很多方法可以这样做,而不是将所有内容提升到 IO
, , 一个简单的 Writer
Monad将足以满足,这等同于使功能返回其实际结果的2核心和一个累加器值(通常是某种列表)。
通常也不需要放置 一切 进入单子,只有需要写入“日志”值的函数(以实例),您可以将可能需要进行记录的子表达式取消,使主要逻辑纯净,然后通过结合纯纯度来重新组装整体计算以通常的方式进行功能和记录计算 fmap
S和Whatnot。请记住 Writer
有点遗憾的借口:无法阅读 从 日志,仅写入它,每个计算在逻辑上都独立于其上下文,这使周围的事物更容易。
但是在某些情况下,即使是多个纯粹的功能,也只需将子表达式移至高级效果,然后在pleck中尝试效果很好即可。
如果您想实际检查纯代码的运行时间行为,但是 - 以实例,以找出为什么子表达会分歧 - 一般而言 无法从其他纯代码中这样做- 事实,这本质上是 定义 纯度。因此,在这种情况下,您别无选择,只能使用“外部”纯语言的工具:要么不纯净的功能,例如 unsafePerformPrintfDebugging
- 我的意思是 trace
- 或修改后的运行时环境,例如GHCI调试器。
trace
还倾向于过度评估其印刷的论点,在此过程中失去了许多懒惰的好处。
好吧,由于整个Haskell是围绕懒惰评估的原则(因此计算顺序实际上是非确定性的),因此使用Printf的使用几乎没有意义。
如果REPL+检查结果值确实不足以进行调试,则将所有内容包装到IO中是唯一的选择(但这不是Haskell编程的正确方法)。