通用Lisp的反引号/反单引号:如何使用?

9
我在使用Lisp的反引号读取宏时遇到了问题。每当我尝试编写一个似乎需要使用嵌入式反引号的宏(例如来自Paul Graham的《ANSI Common Lisp》第399页的``(w ,x ,,y)),我就无法弄清楚如何以可以编译的方式编写我的代码。通常,我的代码会接收到一整串错误信息,其中的错误提示为“逗号不在反引号内”。能否有人提供一些指南,告诉我如何编写可以正确评估的代码?
举个例子,我目前需要一个宏,该宏将以描述规则的形式接受一个表单,规则的形式为'(function-name column-index value),并生成一个谓词lambda体来确定特定行的由column-index索引的元素是否满足该规则。如果我使用规则'(< 1 2)调用此宏,则会生成类似于以下内容的lambda体:
(lambda (row)
  (< (svref row 1) 2))

我能提供的最佳翻译如下:

我能尽力给出以下解释:

(defmacro row-satisfies-rule (rule)
  (let ((x (gensym)))
    `(let ((,x ,rule))
       (lambda (row)
         (`,(car ,x) (svref row `,(cadr ,x)) `,(caddr ,x))))))

经过评估,SBCL输出以下错误报告:
; in: ROW-SATISFIES-RULE '(< 1 2)
;     ((CAR #:G1121) (SVREF ROW (CADR #:G1121)) (CADDR #:G1121))
; 
; caught ERROR:
;   illegal function call

;     (LAMBDA (ROW) ((CAR #:G1121) (SVREF ROW (CADR #:G1121)) (CADDR #:G1121)))
; ==>
;   #'(LAMBDA (ROW) ((CAR #:G1121) (SVREF ROW (CADR #:G1121)) (CADDR #:G1121)))
; 
; caught STYLE-WARNING:
;   The variable ROW is defined but never used.

;     (LET ((#:G1121 '(< 1 2)))
;       (LAMBDA (ROW) ((CAR #:G1121) (SVREF ROW (CADR #:G1121)) (CADDR #:G1121))))
; 
; caught STYLE-WARNING:
;   The variable #:G1121 is defined but never used.
; 
; compilation unit finished
;   caught 1 ERROR condition
;   caught 2 STYLE-WARNING conditions
#<FUNCTION (LAMBDA (ROW)) {2497F245}>

如何编写宏来生成所需的代码,特别是如何实现row-satisfies-rule
借鉴Ivijay和discipulus的想法,我修改了这个宏,使其能够编译并正常工作,甚至可以将表单作为参数传递。它与我的原始计划有些不同,因为我确定将row作为参数包含在内会使代码更加流畅。然而,它非常丑陋。有没有人知道如何在不调用eval的情况下清理它,使其能够执行相同的功能?
(defmacro row-satisfies-rule-p (row rule)
  (let ((x (gensym))
        (y (gensym)))
    `(let ((,x ,row)
           (,y ,rule))
       (destructuring-bind (a b c) ,y
         (eval `(,a (svref ,,x ,b) ,c))))))

此外,希望能够提供一些干净、Lispy的方法来使宏生成代码以在运行时正确地评估参数的解释。

1
“gensym”真的有必要吗?在扩展中,您没有引入任何新变量,并且用于生成扩展的变量名称不会导致泄漏。 - smackcrane
1
去掉 gensym,去掉 eval,然后就只剩下三行代码,看起来很漂亮,而且运行正确。你甚至可以去掉 destructuring-bind,让它变成两行代码。 - smackcrane
值得注意的是,实际上这并不需要宏;事实上,函数可能更加合适。 - smackcrane
3个回答

12
首先,Lisp宏具有“解构”参数列表的功能。这是一个不错的特性,意味着你可以使用参数列表(rule)而不用通过(car rule) (cadr rule) (caddr rule)来逐个获取其中的元素。相反,你可以使用参数列表((function-name column-index value)),这样宏就期望一个包含三个元素的列表作为参数,并且列表的每个元素会被绑定到参数列表中对应的符号上。你可以使用这种方式也可以不用,但通常更加便利。
接下来,`, 实际上不起任何作用,因为反引号告诉Lisp不要评估后面的表达式,而逗号告诉它在最后进行评估。我认为你想要的只是,(car x),它会评估(car x)。如果你使用解构参数,这也不是什么问题。
而且,由于在宏扩展中没有引入任何新变量,我认为在这种情况下不需要使用(gensym)
所以我们可以像这样重写宏:
(defmacro row-satisfies-rule ((function-name column-index value))
  `(lambda (row)
     (,function-name (svref row ,column-index) ,value)))

它会按照您期望的方式进行扩展:

(macroexpand-1 '(row-satisfies-rule (< 1 2)))
=> (LAMBDA (ROW) (< (SVREF ROW 1) 2))

希望这可以帮到你!


如果你需要对参数进行求值以得到规则集合,那么下面是一种不错的方法:

(defmacro row-satisfies-rule (rule)
  (destructuring-bind (function-name column-index value) (eval rule)
    `(lambda (row)
       (,function-name (svref row ,column-index) ,value))))

这是一个例子:

(let ((rules '((< 1 2) (> 3 4))))
  (macroexpand-1 '(row-satisfies-rule (car rules))))
=> (LAMBDA (ROW) (< (SVREF ROW 1) 2))

与之前一样。


如果你想在宏中包含row,并且希望它直接给出答案而不是制作一个函数来执行此操作,请尝试以下方法:

(defmacro row-satisfies-rule-p (row rule)
  (destructuring-bind (function-name column-index value) rule
    `(,function-name (svref ,row ,column-index) ,value)))

如果您需要评估rule参数(例如传递'(< 1 2)(car rules)而不是(< 1 2)),那么只需使用(destructuring-bind(function-name column-index value)(eval rule)即可。


实际上,对于您要执行的操作,函数似乎比宏更合适。简单地

(defun row-satisfies-rule-p (row rule)
  (destructuring-bind (function-name column-index value) rule
    (funcall function-name (svref row column-index) value)))

使用lambda函数的方式与宏相同,但更加简洁,不需要担心反引号混乱的问题。

通常来说,如果能用函数实现的事情就不应该使用宏,这是一种不好的Lisp编程风格。


当我尝试在规则列表中调用这个宏时,比如(row-satisfies-rule (car rules)),我会得到一个错误,提示(car rules)不能正确映射到三个变量。我该怎么解决呢? - sadakatsu
@gamecoder 问题在于宏不会评估它们的参数,因此它尝试将 function-name 绑定到 car,将 column-index 绑定到 rules(而且没有 value,因此出现错误)。在这种情况下,我认为您无法使用解构参数列表,您必须使用单个参数并对其进行评估以获取规则集。我已经在我的答案中添加了一种很好的方法来做到这一点(它本质上与 lvijay 在他的答案中使用的相同)。 - smackcrane
@discuplus:我在哪里可以学到好的Lisp编程风格?我只开始学了六个月,还是有些手忙脚乱。我的代码相当丑陋,而我看到的Lisp示例却非常优美 : / - sadakatsu
1
@gamecoder 嗯,我不能完全说自己已经完全理解好的Lisp风格(至少还没有),但是我所知道的关于Lisp的一切都是从《实用的Common Lisp》和Stack Overflow上学到的。我最好的建议是继续编写代码,如果它不工作或者看起来很奇怪,就寻求批评;就像你在这里做的一样。你会开始逐渐掌握如何编写漂亮(而且有效)的Lisp代码。 - smackcrane

6

需要明白的一件事是反引号功能与宏完全无关。 它可以用于列表创建。 由于源代码通常由列表组成,因此在宏中使用它可能很方便。

CL-USER 4 > `((+ 1 2) ,(+ 2 3))
((+ 1 2) 5)

反引号引入一个引用列表。逗号取消引用:逗号后的表达式会被求值并插入结果。逗号属于反引号:逗号只在反引号表达式内有效。

请注意,这严格来说是Lisp阅读器的一个特性。

上述基本类似于:

CL-USER 5 > (list '(+ 1 2) (+ 2 3))
((+ 1 2) 5)

这将创建一个新列表,其中包含第一个表达式(因为被引用,所以不会被评估),以及第二个表达式的结果。 Lisp 为什么提供反引号符号? 因为当我们想要创建大多数元素未被评估,但有少数元素已被评估的列表时,它提供了一个简单的模板机制。此外,反引号列表看起来类似于结果列表。

4

解决这个问题不需要嵌套反引号。同样,在使用宏时,您不必引用参数。因此,(row-satisfies-rule (< 1 2))(row-satisfies-rule '(< 1 2))更符合Lisp风格。

(defmacro row-satisfies-rule (rule)
  (destructuring-bind (function-name column-index value) rule
    `(lambda (row)
       (,function-name (svref row ,column-index) ,value))))

将解决第一种形式的所有调用问题。解决第二种形式的问题留作练习。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接