来自Ocaml社区,我正在尝试学习一些Haskell。过渡进展顺利,但我对调试有点困惑。我曾经把(很多)“printf”放在我的ocaml代码中,检查一些中间值,或者作为标志来查看计算完全失败的位置。
由于printf是 IO 操作,我是否必须解除 IO monad中的所有haskell代码才能进行此类调试?或者有更好的方法来做到这一点(如果可以避免,我真的不想手工完成)
我还找到跟踪功能: http://www.haskell.org/haskellwiki/Debugging#Printf_and_friends 这似乎正是我想要的,但我不明白它的类型:在任何地方都没有 IO ! 有人能解释一下跟踪功能的行为吗?
答案 0 :(得分:54)
trace
是最容易使用的调试方法。由于您指出的原因,它不在IO
中:无需在IO
monad中提升您的代码。它像这样实现
trace :: String -> a -> a
trace string expr = unsafePerformIO $ do
putTraceMsg string
return expr
因此幕后有IO,但unsafePerformIO
用于逃避它。这是一个可能会破坏参照透明度的函数,您可以猜测它的类型IO a -> a
及其名称。
答案 1 :(得分:17)
trace
简直是不纯洁的。 IO
monad的要点是保持纯度(类型系统没有注意到IO)并定义语句的执行顺序,否则通过惰性求值几乎不会定义。
然而,冒着自己的风险,你可以将一些IO a -> a
混在一起,即执行不纯的IO。这是一个黑客,当然“受到懒惰评估”的影响,但这就是跟踪只是为了调试而做的。
尽管如此,你应该采用其他方式进行调试:
减少调试中间值的需要
使用断点等(基于编译器的调试)
使用通用monad。如果你的代码是monadic,那么把它写成独立于具体的monad。使用type M a = ...
代替普通IO ...
。之后你可以通过变换器轻松组合monad并在它上面放置一个调试monad。即使对monad的需求消失了,你也可以只为纯值插入Identity a
。
答案 2 :(得分:14)
对于它的价值,实际上有两种“调试”问题在这里:
在严格的命令式语言中,这些通常是一致的。在Haskell中,他们通常不会:
如果你只想保留中间值的日志,有很多方法可以做到 - 例如,不是将所有内容都放到IO
中,一个简单的Writer
monad就足够了,这个相当于使函数返回其实际结果的2元组和累加器值(通常是某种列表)。
通常也不需要将所有放入monad中,只需要写入“log”值的函数 - 例如,你可以只考虑可能需要的子表达式要做日志记录,让主逻辑保持纯净,然后通过将fmap
s和诸如此类的常规方式组合纯函数和日志记录计算来重新组合整体计算。请记住,Writer
对于monad来说是一个令人遗憾的借口:无法从日志中读取,只写入它,每个计算在逻辑上都与其上下文无关,让你更容易处理周围的事情。
但是在某些情况下即使是过度杀戮 - 对于许多纯函数来说,只需将子表达式移到顶层并在REPL中尝试一下就可以了。
如果你想实际检查纯代码的运行时行为,但是 - 例如,找出子表达式偏离的原因 - 通常无法从其他纯代码中执行此操作 - 实际上,这基本上是纯度的定义。因此,在这种情况下,您别无选择,只能使用纯语言“外部”存在的工具:要么使用诸如unsafePerformPrintfDebugging
之类的函数 - 错误,我的意思是trace
- 或者修改后的运行时环境,例如GHCi调试器。
答案 3 :(得分:2)
trace
也倾向于过度评估其打印论点,在此过程中失去了很多懒惰的好处。
答案 4 :(得分:0)
如果您可以等到程序完成后再研究输出,那么堆叠Writer monad是实现记录器的经典方法。我使用此here从不纯的HDBC代码返回结果集。
答案 5 :(得分:-4)
好吧,因为整个Haskell是围绕惰性求值的原则构建的(因此计算的顺序实际上是非确定性的),所以使用printf在其中没有多大意义。
如果REPL +检查结果值对于您的调试来说真的不够,那么将所有内容包装到IO中是唯一的选择(但它不是Haskell编程的正确方法)。