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