10.1 求值 (Eval)
如何产生表达式是很直观的:调用 list
即可。我们没有考虑到的是,如何使 Lisp 将列表视为代码。这之间缺少的一环是函数 eval
,它接受一个表达式,将其求值,然后返回它的值:
> (eval '(+ 1 2 3))
6
> (eval '(format t "Hello"))
Hello
NIL
如果这看起很熟悉的话,这是应该的。这就是我们一直交谈的那个 eval
。下面这个函数实现了与顶层非常相似的东西:
(defun our-toplevel ()
(do ()
(nil)
(format t "~%> ")
(print (eval (read)))))
也是因为这个原因,顶层也称为读取─求值─打印循环 (read-eval-print loop, REPL)。
调用 eval
是跨越代码与列表界线的一种方法。但它不是一个好方法:
- 它的效率低下:
eval
处理的是原始列表 (raw list),或者当下编译它,或者用直译器求值。两种方法都比执行编译过的代码来得慢许多。 - 表达式在没有词法语境 (lexical context)的情况下被求值。举例来说,如果你在一个
let
里调用eval
,传给eval
的表达式将无法引用由let
所设置的变量。
有许多更好的方法 (下一节叙述)来利用产生代码的这个可能性。当然 eval
也是有用的,唯一合法的用途像是在顶层循环使用它。
对于程序员来说, eval
的主要价值大概是作为 Lisp 的概念模型。我们可以想像 Lisp 是由一个长的 cond
表达式定义而成:
(defun eval (expr env)
(cond ...
((eql (car expr) 'quote) (cdr expr))
...
(t (apply (symbol-function (car expr))
(mapcar #'(lambda (x)
(eval x env))
(cdr expr))))))
许多表达式由预设子句 (default clause)来处理,预设子句获得 car
所引用的函数,将 cdr
所有的参数求值,并返回将前者应用至后者的结果。 [1]
但是像 (quote x)
那样的句子就不能用这样的方式来处理,因为 quote
就是为了防止它的参数被求值而存在的。所以我们需要给 quote
写一个特别的子句。这也是为什么本质上将其称为特殊操作符 (special operator): 一个需要被实现为 eval
的一个特殊情况的操作符。
函数 coerce
与 compile
提供了一个类似的桥梁,让你把列表转成代码。你可以 coerce
一个 lambda 表达式,使其成为函数,
> (coerce '(lambda (x) x) 'function)
#<Interpreted-Function BF9D96>
而如果你将 nil
作为第一个参数传给 compile
,它会编译作为第二个参数传入的 lambda 表达式。
> (compile nil '(lambda (x) (+ x 2)))
#<Compiled-Function BF55BE>
NIL
NIL
由于 coerce
与 compile
可接受列表作为参数,一个程序可以在动态执行时 (on the fly)构造新函数。但与调用 eval
比起来,这不是一个从根本解决的办法,并且需抱有同样的疑虑来检视这两个函数。
函数 eval
, coerce
与 compile
的麻烦不是它们跨越了代码与列表之间的界线,而是它们在执行期做这件事。跨越界线的代价昂贵。大多数情况下,在编译期做这件事是没问题的,当你的程序执行时,几乎不用成本。下一节会示范如何办到这件事。