Graham的Ansi Common Lisp:第170页,无法理解示例

时间:2018-11-21 05:41:40

标签: lisp common-lisp

(defmacro random-choice (&rest exprs)
    `(case (random ,(length exprs))
        ,@(let ((key -1))
            (mapcar #'(lambda (expr)
                        `(,(incf key) ,expr))
                    exprs))))      

因此,我对此功能执行了macroexpand-1,并且我大致了解了此宏的工作原理,但是我对Graham如何嵌套反引号`,以及他如何使用,@扩展案例感到非常困惑。

  • 什么时候可以嵌套反引号?
  • 在此示例中,为什么格雷厄姆嵌套了反引号?
  • 为什么,@将案例扩展为(random ,(length exprs))案例?
  • 我知道mapcar主要是为了使我们可以增加key,但是此宏如何知道总共mapcar次应用(random ,(length exprs))次呢?
  • 逗号,@正在拼接的隐式列表是如何形成的?

请注意,我非常愚蠢,请以最基本的术语进行解释。

编辑:

我现在了解到,最里面的反引号(,(incf key) ,expr)确保首先评估此函数,因此其大致等效于(list (incf key) expr),然后

,@(let ((i 0))
    (mapcar #'(lambda (expr)
        `(,(incf i) ,expr))
        args))

被评估为列表'((0 a_0) (1 a_1) ... (n a_n))之类的东西,由于我们有,@,所以它被“拼接”到

((0 a_0))
((1 a_n))
    .
    .
    .
((n a_n))

最近(case (random ,(length exprs))被评估为 case (random n),它也为我们提供了外部括号,让我们可以保持

(case (random n)
    ((0 a_0))
    ((1 a_n))
        .
        .
        .
    ((n a_n)))

我知道事件的顺序正确吗?我在网上找不到任何资源可以验证,格雷厄姆的书也没有将其分解成这样。

2 个答案:

答案 0 :(得分:7)

另一种编写方式(摆脱LET,MAPCAR和副作用INCF代码):

CL-USER 44 > (defmacro random-choice (&rest exprs &aux (n (length exprs)))
               `(case (random ,n)
                  ,@(loop for ci below n and expr in exprs
                          collect `(,ci ,expr))))
RANDOM-CHOICE

CL-USER 45 > (macroexpand-1 '(random-choice 10 21 32 43))

(CASE (RANDOM 4) (0 10) (1 21) (2 32) (3 43))

宏使用内部包含计算的反引号形式。我们可以提取计算并将零件分配给变量:

CL-USER 46 > (defmacro random-choice (&rest exprs &aux (n (length exprs)))
               (let ((keyform `(random ,n))
                     (clauses (loop for ci below n and expr in exprs
                                    collect `(,ci ,expr))))
                 `(case ,keyform
                    ,@clauses)))
RANDOM-CHOICE

CL-USER 47 > (macroexpand-1 '(random-choice 10 21 32 43))

(CASE (RANDOM 4) (0 10) (1 21) (2 32) (3 43))

如您所见,反引号形式可以独立计算,最后以反引号形式组合。

当宏函数包含代码片段时,最好将它们保留为带引号或反引号的形式->这样可以使它们更易于在宏函数中识别。用列表计算(使用listcons,...)替换它们不太方便,也不太可读。但是随后需要正确地对反引号/反引号进行排序。在我的示例中,它稍微容易一些,因为零件是独立计算的。这有助于理解宏,因为它与case的语法更加匹配:

CASE keyform {normal-clause}* [otherwise-clause]

normal-clause::= (keys form*) 

在这里,我们仅使用keyform{normal-clause}*中的0..n-1子句。我们也不要使用otherwise-clause

答案 1 :(得分:4)

  

什么时候可以嵌套反引号?

您始终可以嵌套反引号。但是请注意,此代码不会嵌套它们:

`(foo ; in 1 backquote here
   ,@(cons 'bar ; in 0 backquotes here
           `(baz ; in 1 backquotes here
              ,(random 3))))

嵌套反引号如下:

`(let ((x `(,,y ,z))) ...)
  

在此示例中,为什么格雷厄姆嵌套了反引号?

他没有嵌套它们。他使用第一个反引号生成案例的外部主体,然后将其填充以mapcar生成的案例。为了编写每种情况下要生成的代码,他使用了第二个反引号

  

为什么,@将案例扩展为(random ,(length exprs))案例?

它不会那样做。它扩展为(length exprs)个案例。严格地,它合并在其内部内容返回的事物列表中,在这种情况下,是一个表达式列表。

  

我知道mapcar的主要作用是使我们可以增加密钥,但是此宏如何知道总共(random ,(length exprs))次应用mapcar?

这不是mapcar的用途或用途。

  

逗号,@正在拼接的隐式列表是如何形成的?

这就是mapcar的作用。


要解决您的修改问题,您可以将某些事情纠正一半,但paren太多。

mapcar将函数按顺序应用于列表的每个元素,将结果收集到列表中:

CL-USER> (mapcar #'1+ '(1 2 3))
(2 3 4)
CL-USER>(let ((key -1))
          (mapcar (lambda (x)
                    `(,incf key) ,x))
                  '(foo bar (baz wazoo)))
((0 FOO) (1 BAR) (2 (BAZ WAZOO)))

这将进入case表达式的主体:如果随机值是0,则返回FOO,如果它是1,则返回BAR,依此类推。

要获取此随机值,请对0到2之间(含2和2)的随机整数执行(random 3)