请考虑以下代码:
CL-USER> (defmacro sum (a b)
(+ a b))
SUM
CL-USER> (let ((alpha 3) (beta -1))
(sum alpha beta))
; in: LET ((ALPHA 3) (BETA -1))
; (SUM ALPHA BETA)
;
; caught ERROR:
; during macroexpansion of (SUM ALPHA BETA). Use *BREAK-ON-SIGNALS* to intercept.
;
; Argument X is not a NUMBER: ALPHA
; (LET ((ALPHA 3) (BETA -1))
; (SUM ALPHA BETA))
;
; caught STYLE-WARNING:
; The variable ALPHA is defined but never used.
;
; caught STYLE-WARNING:
; The variable BETA is defined but never used.
;
; compilation unit finished
; caught 1 ERROR condition
; caught 2 STYLE-WARNING conditions
; Evaluation aborted on #<SB-INT:COMPILED-PROGRAM-ERROR {10030E4223}>.
基本上有两个原因(我能想到)导致这段代码失败:
1.宏sum
首先根据两个变量alpha
和beta
进行评估,这两个变量作为符号发送到宏中。因此,要在宏中计算的代码是:
(+ 'alpha 'beta)
这是行不通的,因为我们不能添加两个符号。
2.变量alpha
和beta
是词法绑定的,因此宏的代码无法访问alpha
和beta
的符号值。
因此,重新定义sum
:
CL-USER> (defmacro sum (a b)
(+ (symbol-value a) (symbol-value b)))
WARNING: redefining COMMON-LISP-USER::SUM in DEFMACRO
SUM
在这里,宏sum
将评估提供给它的符号的值。它只有在符号范围内才能做到这一点。因此,为了做到这一点,我们可以动态绑定alpha
和beta
。
此外,为了检查动态绑定是否正常工作,我们可以创建一个函数dynamic-checker
,定义如下:
CL-USER> (defun dynamic-checker ()
(+ alpha beta))
; in: DEFUN DYNAMIC-CHECKER
; (+ ALPHA BETA)
;
; caught WARNING:
; undefined variable: ALPHA
;
; caught WARNING:
; undefined variable: BETA
;
; compilation unit finished
; Undefined variables:
; ALPHA BETA
; caught 2 WARNING conditions
DYNAMIC-CHECKER
最后,我们可以在 REPL 中评估此代码:
CL-USER> (let ((alpha 3) (beta -1))
(declare (special alpha))
(declare (special beta))
(print (dynamic-checker))
(sum alpha beta))
这给了我们错误:
; in: LET ((ALPHA 3) (BETA -1))
; (SUM ALPHA BETA)
;
; caught ERROR:
; during macroexpansion of (SUM ALPHA BETA). Use *BREAK-ON-SIGNALS* to intercept.
;
; The variable ALPHA is unbound.
;
; compilation unit finished
; caught 1 ERROR condition
2 ; Evaluation aborted on #<SB-INT:COMPILED-PROGRAM-ERROR {1003F23AD3}>.
CL-USER>
请注意错误代码末尾的2
。这是由函数dynamic-checker
返回的,它添加了alpha
和beta
,即使它们不是它的参数,这也证明了变量alpha
和beta
可以被let
的成员动态访问。
因此,宏sum
现在应该可以工作,因为之前发生的两个问题都已解决。
但事实显然并非如此,我错过了一些东西。
任何帮助表示赞赏。
解释器和编译器与交互性
Common Lisp 允许解释器和编译器。您可以交互式地使用 read-eval-print-loop 并不意味着实现使用解释器。它可以增量编译代码,然后调用编译的代码。Lisp 解释器从 Lisp 表示中运行代码。默认情况下,SBCL 不使用解释器。它使用编译器。
使用解释器
LispWorks有一个解释器。让我们使用它:
CL-USER 8 > (defun test ()
(let ((alpha 3) (beta -1))
(declare (special alpha))
(declare (special beta))
(print (dynamic-checker))
(sum alpha beta)))
TEST
CL-USER 9 > (test)
2
2
因此,代码是有效的,因为 Lisp 解释器执行表单,当它看到一个宏时,它会随时随地扩展它。绑定可用。
让我们使用 LispWorks 步进器,它使用解释器。:s
是步骤命令。
(step (test))
(TEST) -> :s
(LET ((ALPHA 3) (BETA -1))
(DECLARE (SPECIAL ALPHA))
(DECLARE (SPECIAL BETA))
(PRINT (DYNAMIC-CHECKER))
(SUM ALPHA BETA)) -> :s
3 -> :s
3
-1 -> :s
-1
(PRINT (DYNAMIC-CHECKER)) -> :s
(DYNAMIC-CHECKER) -> :s
(+ ALPHA BETA) -> :s
ALPHA -> :s
3
BETA -> :s
-1
2
2
2 ; <- output
2
(SUM ALPHA BETA) <=> 2 ; <- macro expansion to 2
2 -> :s
2 ; 2 evaluates to itself
2
2
2
编译失败
但是我们无法编译您的代码:
CL-USER 10 > (compile 'test)
Error: The variable ALPHA is unbound.
1 (continue) Try evaluating ALPHA again.
2 Return the value of :ALPHA instead.
3 Specify a value to use this time instead of evaluating ALPHA.
4 Specify a value to set ALPHA to.
5 (abort) Return to level 0.
6 Return to top loop level 0.
Type :b for backtrace or :c <option number> to proceed.
Type :bug-form "<subject>" for a bug report template or :? for other options.
编译器尝试展开宏,但不运行代码。由于不执行LET
形式(编译器仅将其编译为其他内容,但不执行它),因此alpha
没有绑定。
风格
通常,最好避免从封闭代码编写需要运行时状态的宏。此类宏只能由解释器执行。
根据用户jkiiski的评论,我觉得您需要了解这两个操作(宏操作和程序/函数应用程序)何时在通常编译的代码中发生。 此外,我不建议编写您定义的宏,但出于教育目的,我将通过它们进行研究。
让我们首先显式声明两个符号是特殊的,并将它们绑定到一些值:
CL-USER> (defvar alpha 6)
6
CL-USER> (defvar beta 5)
5
这不仅将两个变量建立为特殊/动态变量,而且还使它们分别绑定到值 6 和 5。
更重要的是,通过这样做,我们可以在评估宏或代码中的示例之前建立变量和绑定。
现在我们运行您的最终表单,我们应该得到以下内容:
CL-USER> (let ((alpha 3) (beta -1))
(declare (special alpha))
(declare (special beta))
(print (dynamic-checker))
(sum alpha beta))
2
11
由于打印了(dynamic-checker)
,打印了第一个值 2。 返回值 11 是因为宏sum
解析为值 11,然后有效地取代了形式(sum alpha beta)
,并在(let ...)
代码的实际计算中返回。
要意识到的重要一点是,宏是在编译时计算的:也就是说,在编译和运行最终形式的(let ...)
之前。 因为它是在(let ...)
表单之前计算的,所以 let 表单不会在宏解析时建立 3 和 -1 与符号alpha
和beta
的绑定。 在此之前,我一直厚颜无耻地对值 6 和 5 进行了显式绑定,这就是宏所看到的。
成功解析宏后,let 窗体实际上将变为:
(let ((alpha 3) (beta -1))
(declare (special alpha))
(declare (special beta))
(print (dynamic-checker))
11)
这就解释了为什么我们现在没有错误,是什么导致了你上一个示例中的错误,以及为什么我们从上面指定的添加中获得输出,我们可以明确地看到何时解析宏/let 表单。