McCarthy 的基本 S-函数和谓词是atom
, eq
, car
, cdr
,cons
然后,他继续添加到他的基本符号,以便能够编写他所谓的 S-Function:quote
, cond
, lambda
,label
在此基础上,我们将这些称为“LISP 原语”(尽管我对关于类型谓词的争论持开放态度,例如numberp
)
您将如何defmacro
在您选择的 LISP 中仅使用这些原语来定义函数?(包括 Scheme 和 Clojure)
尝试在像 McCarthy 的 LISP 机器这样的机器上执行此操作的问题在于,没有办法防止在运行时进行参数评估,也没有办法在编译时改变事情(这就是宏所做的:它们重新排列代码在编译之前,基本上)。
但这并不能阻止我们在运行时在 McCarthy 的机器上重写我们的代码。诀窍是引用我们传递给“宏”的参数,这样它们就不会被评估。
作为一个例子,让我们看一个我们可能想要的函数;unless
. 我们的理论函数有两个参数,p
and q
,并且返回q
,除非 p
为真。如果p
为真,则返回 nil。
一些例子(在 Clojure 的语法中,但这并没有改变任何东西):
(unless (= "apples" "oranges") "bacon")
=> "bacon"
(unless (= "pears" "pears") "bacon")
=> nil
所以一开始我们可能想写成unless
一个函数:
(defn unless [p q]
(cond p nil
true q))
这似乎工作得很好:
(unless true 6)
=> nil
(unless false 6)
=> 6
使用 McCarthy 的 LISP,它可以正常工作。问题是我们在现代 Lisps 中不仅有无副作用的代码,因此传递给的所有参数unless
都被评估,无论我们是否想要它们,这一事实是有问题的。事实上,即使在 McCarthy 的 LISP 中,如果评估其中一个论点需要很长时间才能完成,而我们只想很少这样做,这也可能是一个问题。但这尤其是一个副作用问题。
所以我们希望我们只在为假unless
时评估和返回。如果我们将和作为参数传递给函数,我们就无法做到这一点。q
p
q
p
但是我们可以quote
在将它们传递给我们的函数之前对其进行处理,从而阻止它们的评估。我们可以使用eval
(也定义了,仅使用原语和其他在参考论文后面的原语中定义的函数)的力量来评估我们需要什么,什么时候需要。
所以我们有一个新的unless
:
(defn unless [p q]
(cond (eval p) nil
true (eval q)))
我们使用它有点不同:
(unless (quote false) (quote (println "squid!")))
=> "squid" nil
(unless (quote true) (quote (println "squid!")))
=> nil
在那里你有一个可以慷慨地称为宏的东西。
但这不是defmacro
或其他语言中的等价物。那是因为在 McCarthy 的机器上,没有办法在编译时执行代码。而且,如果您正在使用该eval
函数评估您的代码,它就无法知道不评估“宏”函数的参数。阅读和评估之间的区别与现在不同,尽管想法就在那里。“重写”代码的能力是存在的,在与quote
结合的酷和列表操作中eval
,但它并没有像现在这样被保留在语言中(我几乎称它为语法糖:只需引用你的论点,你就有了宏观系统的力量。)
我希望我已经回答了你的问题,而不是试图defmacro
用这些原语来定义一个体面的东西。如果您真的想看到这一点,我会向您指出 Clojure 源代码中难以理解的源defmacro
代码,或者 Google 更多。
在这里完整地解释它的所有细节需要大量的空间和时间来回答,但大纲真的很简单。每个 LISP 最终在其核心中都有类似于 READ-EVAL-PRINT 循环的东西,也就是说,它需要一个列表,一个元素一个元素,解释它,并改变状态——无论是在内存中还是通过打印结果。
读取部分查看读取的每个元素并对其进行处理:
(cond ((atom elem)(lambda ...))
((function-p elem) (lambda ...)))
要解释宏,您只需 (?) 需要实现一个函数,将宏的模板文本放在存储中的某个位置,该 repl 循环的谓词——这意味着简单地定义一个函数——它说“哦,这是一个宏!”,然后将该模板文本复制回阅读器,以便对其进行解释。
如果您真的想了解细节,请阅读计算机程序的结构和解释 或阅读 Queinnec 的Lisp in Small Pieces。