跳转到内容

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 bar 而不是 show foo 作为正确的跟踪訊息。

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 进行调试

[编辑 | 编辑源代码]

一般提示

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