2

原文: http://en.wikibooks.org/wiki/Haskell/Debugging

借助 Debug.Trace 打印调试信息

打印调试信息是调试程序通用的办法.
命令式语言中, 我们只要设置打印语句到标准输出的代码,
或者到某些 log 文件里, 这样就能追踪调试信息了.
(比如说, 特定变量的值, 或者一些肉眼可读的消息.)
然而对于 Haskell, 除了 IO Monad 以外我们没办法打印任何信息.,
而且我们不像单单为了调试引入那样的例外.

为了解决这个问题, 标准库提供了 Debug.Trace 模块.
模块导出了一个 trace 函数, 提供了一个简便的方法在程序任何位置添加调试信息.
比如说, 下面的程序会打印出每个传给 fib 的不等于 01 的参数:

haskellmodule 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 同时实现了追踪程序执行步骤, 就是, 比如说, 哪个函数先执行, 哪个后执行.
办法是, 我们在关注的函数里某些部分添加代码, 像这样:

haskallmodule 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 Monad 以外使用, 实际上类型声明是...

haskelltrace :: String -> a -> a

...意味着这是个纯函数. 但确实, trace 打印可用信息时在操作 IO. 怎么回事?
实际上, trace 使用了一个鬼把戏绕过了 IO 和纯的 Haskell 之间的隔离.
这在下面的免责声明里有体现, 见trace 的文档:

trace 函数只应该用于调试, 或者监视执行过程.
这个函数并不是引用透明的: 其类型表明是一个纯函数, 但它存在输出 trace 信息的副作用.

一个 trace 常见的错误用法:
为了让调试追踪信息适应已有的函数, 意外把 trace 消息中计算出来打印包含进来.
比如, 不要写出这样的代码:

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

这会造成无限递归, 因为追踪信息会先于 bar 表达式求值,
导致按照追踪信息里 foo 的执行, 然后又是 bar,
然后追踪信息要在 bar 前求值, 这样直到无穷.
正确的追踪信息应该用 show bar 而不是 show foo:

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

实用的风格

一个揉合进 show 的辅助函数很方便:

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

同样地, Debug.Trace 定义了一个 traceShow 函数,
这个函数打印第一个参数, 并且求值到第二个参数(evaluates to the second one):

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

最后, 一个这样的 debug 函数也证明很便捷:

haskelldebug = flip trace

它能让你这样写代码...

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

...让注释/反注释调试语句变得更加容易.


题叶
17.3k 声望2.6k 粉丝

Calcit 语言作者


引用和评论

0 条评论