如果这看起很熟悉的话,这是应该的。这就是我们一直交谈的那个 eval
。下面这个函数实现了与顶层非常相似的东西:
也是因为这个原因,顶层也称为读取─求值─打印循环 (read-eval-print loop, REPL)。
调用 eval
是跨越代码与列表界线的一种方法。但它不是一个好方法:
对于程序员来说, eval
的主要价值大概是作为 Lisp 的概念模型。我们可以想像 Lisp 是由一个长的 cond
表达式定义而成:
许多表达式由预设子句 (default clause)来处理,预设子句获得 car
所引用的函数,将 cdr
所有的参数求值,并返回将前者应用至后者的结果。
但是像 (quote x)
那样的句子就不能用这样的方式来处理,因为 quote
就是为了防止它的参数被求值而存在的。所以我们需要给 quote
写一个特别的子句。这也是为什么本质上将其称为特殊操作符 (special operator): 一个需要被实现为 eval
的一个特殊情况的操作符。
而如果你将 nil
作为第一个参数传给 compile
,它会编译作为第二个参数传入的 lambda 表达式。
由于 coerce
与 compile
可接受列表作为参数,一个程序可以在动态执行时 (on the fly)构造新函数。但与调用 eval
比起来,这不是一个从根本解决的办法,并且需抱有同样的疑虑来检视这两个函数。
函数 eval
, 与 compile
的麻烦不是它们跨越了代码与列表之间的界线,而是它们在执行期做这件事。跨越界线的代价昂贵。大多数情况下,在编译期做这件事是没问题的,当你的程序执行时,几乎不用成本。下一节会示范如何办到这件事。