【问题标题】:How to "debug" Haskell with printfs?如何使用 printfs “调试” Haskell?
【发布时间】:2010-08-23 10:15:31
【问题描述】:

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

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

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

【问题讨论】:

  • 需要注意的是,trace 仅用于调试,如果您将其用于“真实”逻辑,则社区会避开它。

标签: debugging haskell trace printf-debugging


【解决方案1】:

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

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

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

【讨论】:

    【解决方案2】:

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

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

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

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

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

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

    【讨论】:

      【解决方案3】:

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

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

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

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

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

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

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

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

      【讨论】:

        【解决方案4】:

        trace 也倾向于高估其打印的论点,从而失去了很多懒惰的好处。

        【讨论】:

        • 好吧,那就不要那样做 :-)(即只追踪你有信心强迫的东西)
        【解决方案5】:

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

        【讨论】:

        • 因为懒惰的评估:你实际上不必等到程序完成。
        【解决方案6】:

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

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

        【讨论】:

        • 投反对票,因为您的两个主张都是错误的。在按需调用中,计算顺序是静态确定的。它不仅由函数体决定,还由函数执行的外部上下文决定。它不像数据依赖关系是在运行时确定的,也不是根据运行时值不可预测地直接控制流。至于第二段——除了 REPL 之外还有很多选择:跟踪(包括比 trace 更智能的堆检查器)、测试、ghci 调试器和图形前端(leksah 等)。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2015-09-30
        • 2020-03-14
        • 1970-01-01
        • 2022-10-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多