10.1 求值 (Eval)

如何产生表达式是很直观的:调用 list 即可。我们没有考虑到的是,如何使 Lisp 将列表视为代码。这之间缺少的一环是函数 eval ,它接受一个表达式,将其求值,然后返回它的值:

  1. > (eval '(+ 1 2 3))
  2. 6
  3. > (eval '(format t "Hello"))
  4. Hello
  5. NIL

如果这看起很熟悉的话,这是应该的。这就是我们一直交谈的那个 eval 。下面这个函数实现了与顶层非常相似的东西:

  1. (defun our-toplevel ()
  2. (do ()
  3. (nil)
  4. (format t "~%> ")
  5. (print (eval (read)))))

也是因为这个原因,顶层也称为读取─求值─打印循环 (read-eval-print loop, REPL)。

调用 eval 是跨越代码与列表界线的一种方法。但它不是一个好方法:

  1. 它的效率低下: eval 处理的是原始列表 (raw list),或者当下编译它,或者用直译器求值。两种方法都比执行编译过的代码来得慢许多。
  2. 表达式在没有词法语境 (lexical context)的情况下被求值。举例来说,如果你在一个 let 里调用 eval ,传给 eval 的表达式将无法引用由 let 所设置的变量。

有许多更好的方法 (下一节叙述)来利用产生代码的这个可能性。当然 eval 也是有用的,唯一合法的用途像是在顶层循环使用它。

对于程序员来说, eval 的主要价值大概是作为 Lisp 的概念模型。我们可以想像 Lisp 是由一个长的 cond 表达式定义而成:

  1. (defun eval (expr env)
  2. (cond ...
  3. ((eql (car expr) 'quote) (cdr expr))
  4. ...
  5. (t (apply (symbol-function (car expr))
  6. (mapcar #'(lambda (x)
  7. (eval x env))
  8. (cdr expr))))))

许多表达式由预设子句 (default clause)来处理,预设子句获得 car 所引用的函数,将 cdr 所有的参数求值,并返回将前者应用至后者的结果。 [1]

但是像 (quote x) 那样的句子就不能用这样的方式来处理,因为 quote 就是为了防止它的参数被求值而存在的。所以我们需要给 quote 写一个特别的子句。这也是为什么本质上将其称为特殊操作符 (special operator): 一个需要被实现为 eval 的一个特殊情况的操作符。

函数 coercecompile 提供了一个类似的桥梁,让你把列表转成代码。你可以 coerce 一个 lambda 表达式,使其成为函数,

  1. > (coerce '(lambda (x) x) 'function)
  2. #<Interpreted-Function BF9D96>

而如果你将 nil 作为第一个参数传给 compile ,它会编译作为第二个参数传入的 lambda 表达式。

  1. > (compile nil '(lambda (x) (+ x 2)))
  2. #<Compiled-Function BF55BE>
  3. NIL
  4. NIL

由于 coercecompile 可接受列表作为参数,一个程序可以在动态执行时 (on the fly)构造新函数。但与调用 eval 比起来,这不是一个从根本解决的办法,并且需抱有同样的疑虑来检视这两个函数。

函数 eval , coercecompile 的麻烦不是它们跨越了代码与列表之间的界线,而是它们在执行期做这件事。跨越界线的代价昂贵。大多数情况下,在编译期做这件事是没问题的,当你的程序执行时,几乎不用成本。下一节会示范如何办到这件事。