repos
repos

Reputation: 73

Graham's Ansi Common Lisp: p.170 having trouble understanding example

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

So I performed macroexpand-1 on this function and I understand generally how this macro works, but I'm super confused of how Graham nest the backquote `, and how he uses ,@ to expand the cases.

Note I am very stupid so please explain in the most basic of terms.

EDIT:

I now understand that the inner most backquote (,(incf key) ,expr) ensures that this function is evaluated first so its roughly equivalent to (list (incf key) expr), then

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

is evaluated into something like the list '((0 a_0) (1 a_1) ... (n a_n)), and since we have ,@ then this is 'spliced' into

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

Lastly (case (random ,(length exprs)) is evaluated to case (random n) and it gives us the outer parenthesis as well, leaving us with

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

Did I get the sequence of events correct? I couldn't find any resources online to verify and Graham's book does not break it down like this.

Upvotes: 3

Views: 140

Answers (2)

Dan Robertson
Dan Robertson

Reputation: 4360

When can we nest backquotes?

You can always nest backquotes. However note that this code does not nest them:

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

Nesting backquotes looks like:

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

Why does Graham nest the backquotes in this example?

He does not nest them. He generates the outer body of the case with the first backquotes, then fills it with the cases which are generated with mapcar. To write the code to be generated for each case he uses a second backquote

Why does ,@ expand the the cases into the (random ,(length exprs)) cases?

It does not do that. It expands into (length exprs) cases. Strictly it merges in the list of things returned by whatever is inside it, in this case a list of expressions.

I understand that the mapcar is primarily so that we can increase the key, but how does this macro know to apply mapcar a total of (random ,(length exprs)) times?

That is not what the mapcar does or what it is for.

How is the implicit list that comma-at ,@ is splicing being formed?

This is what mapcar does.


To address your edit, you got some things half correct but you have too many parens.

mapcar applies a function to each element of a list in order, collecting the results into a list:

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)))

This is going go into the body of the case expression: If the random value is 0 then return FOO, if it is 1 then BAR, and so on.

To get this random value you do (random 3) for a random integer between 0 and 2 inclusive.

Upvotes: 4

Rainer Joswig
Rainer Joswig

Reputation: 139251

Another way to write it (getting rid of the LET, MAPCAR + side-effect INCF code):

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))

The macro uses a backquote form with computation inside. We can extract the computation and assign the parts to variables:

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))

As you can see the backquote forms can be computed independently and assembled in the backquote form at the end.

When the macro function contains code snippets, it is preferable to keep them as quoted or backquoted forms -> this makes them easier to identify in the macro function. Replacing them with list computation (using list, cons, ...) is less convenient and less readable. But one then needs to get the sequencing of backquote/unquote correct. In my example it's slightly easier, because the parts are computed independently. This helps to understand the macro, since it matches a bit more the syntax of case:

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

normal-clause::= (keys form*) 

Here we use only the keyform and the 0..n-1 clauses from {normal-clause}*. We also don't use the otherwise-clause.

Upvotes: 7

Related Questions