跳转到内容

Haskell/调试

来自维基教科书,开放的书籍,开放的世界

使用 `Debug.Trace` 进行调试打印

[编辑 | 编辑源代码]

调试打印是调试程序的一种常见方法。在命令式语言中,我们可以简单地在代码中添加打印语句到标准输出或某些日志文件中,以跟踪调试信息(例如,特定变量的值或一些可读的消息)。然而,在 Haskell 中,我们只能通过 IO 单子输出信息;我们不希望仅仅为了调试而引入它。

为了解决这个问题,标准库提供了 Debug.Trace。该模块导出一个名为 `trace` 的函数,它提供了一种方便的方式在程序的任何地方添加调试打印语句。例如,这个程序打印传递给 `fib` 的所有参数,当它们不等于 0 或 1 时

module Main where
import Debug.Trace

fib :: Int -> Int
fib 0 = 0
fib 1 = 1
fib n = trace ("n: " ++ show n) $ fib (n - 1) + fib (n - 2)

main = putStrLn $ "fib 4: " ++ show (fib 4)

以下是结果输出

n: 4
n: 3
n: 2
n: 2
fib 4: 3

此外,`trace` 使得能够跟踪程序的执行步骤;也就是说,哪个函数首先被调用,其次被调用,等等。为此,我们注释我们感兴趣的函数部分,如下所示

module Main where
import Debug.Trace

factorial :: Int -> Int
factorial n | n == 0    = trace ("branch 1") 1
            | otherwise = trace ("branch 2") $ n * (factorial $ n - 1)

main = do
    putStrLn $ "factorial 6: " ++ show (factorial 6)

当以这种方式注释的程序运行时,它将按注释语句执行的顺序打印调试字符串。该输出可能有助于在缺少语句或类似情况的情况下定位错误。

一些额外的建议

[编辑 | 编辑源代码]

如上所示,`trace` 可用于 IO 单子之外;实际上,它的类型签名...

trace :: String -> a -> a

...表明它是一个纯函数。然而,`trace` 确实 在打印有用信息时执行了 IO。发生了什么事?实际上,`trace` 使用了一种技巧来规避 IO 与纯 Haskell 之间的分离。这体现在以下免责声明中,可以在 `trace` 文档 中找到

`trace` 函数应用于调试或监视执行。该函数不是引用透明的:它的类型表明它是一个纯函数,但它具有输出跟踪消息的副作用。

使用 `trace` 的一个常见错误:在尝试将调试跟踪融入现有函数时,人们不小心将要被 `trace` 打印的值包含在要打印的消息中;例如,不要这样做

let foo = trace ("foo = " ++ show foo) $ bar
in  baz

这会导致无限递归,因为跟踪消息将在 bar 表达式之前被评估,这将导致 foo 按照跟踪消息和 bar 的顺序进行评估,而跟踪消息将在 bar 之前被评估,如此无限循环。不要使用 `show foo`,而应在跟踪消息中使用 `show bar`

let foo = trace ("foo = " ++ show bar) $ bar
in  baz

有用的习惯用法

[编辑 | 编辑源代码]

包含 `show` 的辅助函数可能很方便

traceThis :: (Show a) => a -> a
traceThis x = trace (show x) x

类似地,`Debug.Trace` 定义了一个 `traceShow` 函数,它“打印”它的第一个参数并评估为第二个参数

traceShow :: (Show a) => a -> b -> b
traceShow = trace . show

最后,像这样的 `debug` 函数也可能有用

debug = flip trace

这将允许您编写类似的代码...

main = (1 + 2) `debug` "adding"

...使调试语句的注释/取消注释变得更容易。

使用 GHCi 进行增量开发

[编辑 | 编辑源代码]

使用 Hat 进行调试

[编辑 | 编辑源代码]

一般技巧

[编辑 | 编辑源代码]
华夏公益教科书