对于它的价值,这里实际上有两种“调试”问题:
- 记录中间值,例如特定子表达式在每次调用递归函数时的值
- 检查表达式求值的运行时行为
在严格的命令式语言中,这些通常是一致的。在 Haskell 中,他们通常不会:
- 记录中间值可以改变运行时行为,例如通过强制评估否则将被丢弃的术语。
- 由于惰性和共享子表达式,实际计算过程可能与表达式的明显结构有很大不同。
如果您只想保留中间值的日志,有很多方法可以做到这一点 - 例如,与其将所有内容都提升到IO
,一个简单的Writer
monad 就足够了,这相当于让函数返回其实际值的 2 元组结果和累加器值(通常是某种列表)。
通常也不需要将所有内容都放入 monad,只需将需要写入“log”值的函数放入 - 例如,您可以只考虑可能需要进行日志记录的子表达式,而保留主要逻辑,然后通过将纯函数和日志计算以通常的方式与fmap
s 和诸如此类的组合来重新组合整个计算。请记住,这Writer
对于 monad 来说是一种抱歉的借口:无法从日志中读取,只能写入,每次计算在逻辑上都独立于其上下文,这使得处理事情变得更容易。
但在某些情况下,即使这样也太过分了——对于许多纯函数,只需将子表达式移动到顶层并在 REPL 中尝试就可以很好地工作。
但是,如果您想实际检查纯代码的运行时行为——例如,找出子表达式为何发散——通常无法从其他纯代码中这样做——事实上,这本质上是纯度的定义。因此,在这种情况下,您别无选择,只能使用存在于纯语言“之外”的工具:要么是不纯的函数,例如unsafePerformPrintfDebugging
--errr,我的意思是trace
- 要么是修改后的运行时环境,例如 GHCi 调试器。