9

I am having trouble with Lisp's backquote read macro. Whenever I try to write a macro that seems to require the use of embedded backquotes (e.g., ``(w ,x ,,y) from Paul Graham's ANSI Common Lisp, page 399), I cannot figure out how to write my code in a way that compiles. Typically, my code receives a whole chain of errors preceded with "Comma not inside a backquote." Can someone provide some guidelines for how I can write code that will evaluate properly?

As an example, I currently need a macro which takes a form that describes a rule in the form of '(function-name column-index value) and generates a predicate lambda body to determine whether the element indexed by column-index for a particular row satisfies the rule. If I called this macro with the rule '(< 1 2), I would want a lambda body that looks like the following to be generated:

(lambda (row)
  (< (svref row 1) 2))

The best stab I can make at this is as follows:

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

Upon evaluation, SBCL spews the following error report:

; 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}>

How can I write macros to generate the code I need, and in particular, how do I implement row-satisfies-rule?


Using the ideas from Ivijay and discipulus, I have modified the macro so that it compiles and works, even allowing forms to be passed as the arguments. It runs a bit differently from my originally planned macro since I determined that including row as an argument made for smoother code. However, it is ugly as sin. Does anyone know how to clean it up so it performs the same without the call to 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))))))

Also, an explanation of clean, Lispy ways to get macros to generate code to properly evaluate the arguments at runtime would be greatly appreciated.

Rainer Joswig
  • 136,269
  • 10
  • 221
  • 346
sadakatsu
  • 1,255
  • 18
  • 36
  • 1
    Are `gensym`s really necessary? You aren't introducing any new variables in the expansion, and the variable names you use to generate the expansion won't cause leaks. – smackcrane Jun 25 '11 at 04:29
  • 1
    Take away the `gensym`s, take away the `eval` and then it's three lines, looks pretty, and works right. You can even take away the `destructuring-bind` and make it two lines. – smackcrane Jun 25 '11 at 04:33
  • It's worth noting, in fact, that a macro isn't even necessary for this; in fact, a function might be more appropriate. – smackcrane Jun 25 '11 at 04:46

3 Answers3

12

First of all, Lisp macros have "destructuring" argument lists. This is a nice feature that means instead of having an argument list (rule) and then taking it apart with (car rule) (cadr rule) (caddr rule), you can simply make the argument list ((function-name column-index value)). That way the macro expects a list of three elements as an argument, and each element of the list is then bound to the corresponding symbol in the arguemnt list. You can use this or not, but it's usually more convenient.

Next, `, doesn't actually do anything, because the backquote tells Lisp not to evaluate the following expression and the comma tells it to evaluate it after all. I think you meant just ,(car x), which evaluates (car x). This isn't a problem anyway if you use destructuring arguments.

And since you're not introducing any new variables in the macro expansion, I don't think (gensym) is necessary in this case.

So we can rewrite the macro like this:

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

Which expands just how you wanted:

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

Hope this helps!


If you need the argument to be evaluated to get the rule set, then here's a nice way to do it:

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

Here's an example:

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

just like before.


If you want to include row in the macro and have it give you your answer straightaway instead of making a function to do that, try this:

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

Or if you need to evaluate the rule argument (e.g. passing '(< 1 2) or (car rules) instead of (< 1 2)) then just use (destructuring-bind (function-name column-index value) (eval rule)


Actually, a function seems more appropriate than a macro for what you're trying to do. Simply

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

works the same way as the macro and is much neater, without all the backquoting mess to worry about.

In general, it's bad Lisp style to use macros for things that can be accomplished by functions.

smackcrane
  • 1,379
  • 2
  • 10
  • 17
  • When I try calling this macro when my rules are in a list, like `(row-satisfies-rule (car rules))`, I get an error saying that `(car rules)` does not map correctly to three variables. How do I fix this? – sadakatsu Jun 25 '11 at 01:42
  • @gamecoder the problem is that macros don't evaluate their arguments, so it tries to bind `function-name` to `car` and `column-index` to `rules` (and there's nothing for `value`, hence the error). In this case I don't think you can use a destructuring argument list, you'll have to use a single argument and evaluate it to get the rule set. I've added a nice way to do this to my answer (it's essentially the same thing lvijay used in his answer). – smackcrane Jun 25 '11 at 02:32
  • @discuplus: Where can I learn good Lisp style? I've only started about six months ago, and I still fumble about quite a bit. My code is rather grotesque, and the example Lisp I've seen has been beautiful : / – sadakatsu Jun 25 '11 at 15:40
  • 1
    @gamecoder well, I can't claim to completely understand good Lisp style (yet), but everything I know about Lisp I've learned from [Practical Common Lisp](http://www.gigamonkeys.com/book/) and here on stack overflow. My best advice would be to keep writing code and if it doesn't work or it seems weird, ask for critiques; exactly what you're doing here. You'll start to get a feel for how to write pretty (and functioning) Lisp. – smackcrane Jun 25 '11 at 21:06
6

One thing to understand is that the backquote feature is completely unrelated to macros. It can be used for list creation. Since source code usually consists of lists, it may be handy in macros.

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

The backquote introduces a quoted list. The comma does the unquote: the expression after the comma is evaluated and the result inserted. The comma belongs to the backquote: the comma is only valid inside a backquote expression.

Note also that this is strictly a feature of the Lisp reader.

Above is basically similar to:

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

This creates a new list with the first expression (not evaluated, because quoted) and the result of the second expression.

Why does Lisp provide backquote notation?

Because it provides a simple template mechanism when one wants to create lists where most of the elements are not evaluated, but a few are. Additionally the backquoted list looks similar to the result list.

Rainer Joswig
  • 136,269
  • 10
  • 221
  • 346
4

you don't need nested backquotes to solve this problem. Also, when it's a macro, you don't have to quote your arguments. So (row-satisfies-rule (< 1 2)) is lispier than (row-satisfies-rule '(< 1 2)).

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

will solve the problem for all calls in the first form. Solving the problem when in the second form is left as an exercise.

lvijay
  • 41
  • 1