追踪与回溯 (Traces and Backtraces)

当你的程序不如你预期的那样工作时,有时候第一件该解决的事情是,它在做什么?如果你输入 (trace foo) ,则 Lisp 会在每次调用或返回 foo 时显示一个信息,显示传给 foo 的参数,或是 foo 返回的值。你可以追踪任何自己定义的 (user-defined)函数。

一个追踪通常会根据调用树来缩进。在一个做遍历的函数,像下面这个函数,它给一个树的每一个非空元素加上 1,

  1. (defun tree1+ (tr)
  2. (cond ((null tr) nil)
  3. ((atom tr) (1+ tr))
  4. (t (cons (treel+ (car tr))
  5. (treel+ (cdr tr))))))

一个树的形状会因此反映出它被遍历时的数据结构:

  1. > (trace tree1+)
  2. (tree1+)
  3. > (tree1+ '((1 . 3) 5 . 7))
  4. 1 Enter TREE1+ ((1 . 3) 5 . 7)
  5. 2 Enter TREE1+ (1.3)
  6. 3 Enter TREE1+ 1
  7. 3 Exit TREE1+ 2
  8. 3 Enter TREE1+ 3
  9. 3 Exit TREE1+ 4
  10. 2 Exit TREE1+ (2 . 4)
  11. 2 Enter TREE1+ (5 . 7)
  12. 3 Enter TREE1+ 5
  13. 3 Exit TREE1+ 6
  14. 3 Enter TREE1+ 7
  15. 3 Exit TREE1+ 8
  16. 2 Exit TREE1+ (6 . 8)
  17. 1 Exit TREE1+ ((2 . 4) 6 . 8)
  18. ((2 . 4) 6 . 8)

要关掉 foo 的追踪,输入 (untrace foo) ;要关掉所有正在追踪的函数,只要输入 (untrace) 就好。

一个更灵活的追踪办法是在你的代码里插入诊断性的打印语句。如果已经知道结果了,这个经典的方法大概会与复杂的调适工具一样被使用数十次。这也是为什么可以互动地重定义函数式多么有用的原因。

一个回溯 (backtrace)是一个当前存在栈的调用的列表,当一个错误中止求值时,会由一个中断循环生成此列表。如果追踪像是”让我看看你在做什么”,一个回溯像是询问”我们是怎么到达这里的?” 在某方面上,追踪与回溯是互补的。一个追踪会显示在一个程序的调用树里,选定函数的调用。一个回溯会显示在一个程序部分的调用树里,所有函数的调用(路径为从顶层调用到发生错误的地方)。

在一个典型的实现里,我们可通过在中断循环里输入 :backtrace 来获得一个回溯,看起来可能像下面这样:

  1. > (tree1+ ' ( ( 1 . 3) 5 . A))
  2. Error: A is not a valid argument to 1+.
  3. Options: :abort, :backtrace
  4. » :backtrace
  5. (1+ A)
  6. (TREE1+ A)
  7. (TREE1+ (5 . A))
  8. (TREE1+ ((1 . 3) 5 . A))

出现在回溯里的臭虫较容易被发现。你可以仅往回检查调用链,直到你找到第一个不该发生的事情。另一个函数式编程 (2.12 节)的好处是所有的臭虫都会在回溯里出现。在纯函数式代码里,每一个可能出错的调用,在错误发生时,一定会在栈出现。

一个回溯每个实现所提供的信息量都不同。某些实现会完整显示一个所有待调用的历史,并显示参数。其他实现可能仅显示调用历史。一般来说,追踪与回溯解释型的代码会得到较多的信息,这也是为什么你要在确定你的程序可以工作之后,再来编译。

传统上我们在解释器里调试代码,且只在工作的情况下才编译。但这个观点也是可以改变的:至少有两个 Common Lisp 实现没有包含解释器。