How to convert this Common Lisp function into a macro?

153 Views Asked by At

I am using SBCL, Slime, and Emacs to develop in Common Lisp.

I have this function:

(defun build-cond-action-pairs (&rest var)
  (labels ((aux (xs-left accu)
             (cond ((null (cddr xs-left))
                    (append accu (list (list (first xs-left)
                                             (second xs-left)))))
                   (t (aux (cddr xs-left)
                           (append accu (list (list (first xs-left)
                                                    (second xs-left)))))))))
    (aux var nil)))

I also defined these two variables:

CL-USER>(defparameter var-a 1)
VAR-A

CL-USER> (defparameter var-b 1)
VAR-B

When I call the function with:

CL-USER> (build-cond-action-pairs "fish are cool" (incf var-a) "amphibians are cool" (incf var-b))

As expected, the arguments are evaluated:

(("fish are cool" 2) ("amphibians are cool" 2))

I want to transform this function into a macro. Hence, the arguments will not be evaluated.

The desired output result would be:

(("fish are cool" (incf var-a)) ("amphibians are cool" (incf var-b)))

I tried with:


CL-USER> (defmacro macro-build-cond-action-pairs (&rest var)
  `(labels ((aux (,xs-left ,accu)
             (cond ((null (cddr ,xs-left))
                    (append ,accu (list (list (first ,xs-left)
                                             (second ,xs-left)))))
                   (t (aux (cddr ,xs-left)
                           (append ,accu (list (list (first ,xs-left)
                                                    (second ,xs-left)))))))))
    (aux ,var nil)))

But, it does not work:

; in: DEFMACRO MACRO-BUILD-COND-ACTION-PAIRS
;     `(LABELS ((AUX (,XS-LEFT ,ACCU)
;                 (COND (# #) (T #))))
;        (AUX ,VAR NIL))
; --> SB-IMPL::|List| SB-IMPL::|List| SB-IMPL::|List| 
; ==>
;   (SB-IMPL::|List| XS-LEFT ACCU)
; 
; caught WARNING:
;   undefined variable: COMMON-LISP-USER::ACCU
; 
; caught WARNING:
;   undefined variable: COMMON-LISP-USER::XS-LEFT
; 
; compilation unit finished
;   Undefined variables:
;     ACCU XS-LEFT
;   caught 2 WARNING conditions
MACRO-BUILD-COND-ACTION-PAIRS

Feels like a package (or namespace problem). Maybe the root is the labels part. I do not know how to solve it.

How can I fix this?

Thanks.

2

There are 2 best solutions below

0
On BEST ANSWER

@Gwang-JinKim presented a solution (thanks for the help!). However, his solution changes the recursive approach described in my original answer.

I ended up finding a way to fix the macro keeping it very similar to the original question. Basically, it was necessary to remove some commas and to insert a (quote ...) before the tail call.

Check it out:

CL-USER> (defmacro macro-build-cond-action-pairs (&rest var)
        `(labels ((aux (xs-left accu)
                    (cond ((null (cddr xs-left))
                           (append accu (list (list (first xs-left)
                                                     (second xs-left)))))
                          (t (aux (cddr xs-left)
                                  (append accu (list (list (first xs-left)
                                                            (second xs-left)))))))))
           (aux (quote ,var) nil)))

It works:

CL-USER> (macro-build-cond-action-pairs "fish are cool" (incf var-a) "amphibians are cool" (incf var-b))

(("fish are cool" (INCF VAR-A)) ("amphibians are cool" (INCF VAR-B)))

1
On
;; from:
;; (build-cond-action-pairs "fish are cool" (incf var-a) 
;;                          "amphibians are cool" (incf var-b))

;; the macro-expansion should be:
;; (("fish are cool" (incf var-a)) ("amphibians are cool" (incf var-b)))

;; of course, this is makeable - with common lisp.

However, macro's are then executed - and execution of this is not possible because the first position of this list ("fish are cool" (incf var-a)) doesn't return a function.

But if the macro-expansion is

;; (list '("fish are cool" (incf var-a)) '("amphibians are cool" (incf var-b)))
;; which is equivalent to:
;; (list (quote ("fish are cool" (incf var-a))) (quote ("amphibians are cool" (incf var-b))))
;; it would evaluate to:
;; (("fish are cool" (incf var-a)) ("amphibians are cool" (incf var-b)))

This would be regular lisp.

Although it does not expand to a valid lisp expression, we can achieve it even that it expands really to:

(("fish are cool" (incf var-a)) ("amphibians are cool" (incf var-b)))

Because using macroexpand-1 you can check to what the macro expands to - no matter whether the evaluation of the expanded expression will give an error or not. So I will show you both possibilities.

;; we want
;; (macroexpand-1 '(build-cond-action-pairs "fish are cool" (incf var-a) 
;;                                          "amphibians are cool" (incf var-b)))
;; returns:
;; (("fish are cool" (incf var-a)) ("amphibians are cool" (incf var-b)))

(defmacro build-cond-action-pairs (&rest var)
  ...)
;; so we want this macro loops over its var elements and pairs the elements.
;; we need a function which takes a list and generates a list of lists
;; where the inner lists group pairs of elements.
;; One can achieve this with loop.

(defun to-pairs (l)
  "Group elements of list l in lists of length 2 - pairs."
  (loop for (a b &rest x) on l by #'cddr
        collect (list a b)))

;; a macro takes its arguments list and doesn't evaluate its arguments.
;; we can use inside macros such functions to re-arrange the arguments list
;; - we can use list/data manipulation functions to re-arrange code - this
;; is the power of macros in common lisp!

(defmacro build-cond-action-pairs (&rest var)
  `,(to-pairs `,var))

;; try it out:
(macroexpand-1 '(build-cond-action-pairs "fish are cool" (incf var-a) 
                                         "amphibians are cool" (incf var-b)))
;; returning:
(("fish are cool" (INCF VAR-A)) ("amphibians are cool" (INCF VAR-B))) ;
T

So this expands exactly to how you want it. But when executing the macro there will be an error:

(build-cond-action-pairs "fish are cool" (incf var-a) 
                                         "amphibians are cool" (incf var-b))
*** - EVAL: ("fish are cool" (INCF VAR-A)) is not a function name; try using a
      symbol instead
The following restarts are available:
USE-VALUE      :R1      Input a value to be used instead.
ABORT          :R2      Abort main loop

(I tried it out in the implementation clisp of common lisp - just because I use it for very quick tests often - while for serious programming I use emacs + sbcl).

I wanted just to demonstrate you that lisp can do even this.

So let's build the other variant with list and quote:

(defun to-quoted-pairs (l)
  "Group elements of list l in quoted lists of length 2 - quoted pairs."
  (loop for (a b &rest x) on l by #'cddr
        collect (list 'quote (list a b))))

(defmacro build-cond-action-pairs (&rest var)
  `,(to-quoted-pairs `,var))

(macroexpand-1 '(build-cond-action-pairs "fish are cool" (incf var-a) 
                                         "amphibians are cool" (incf var-b)))
;;=> ('("fish are cool" (INCF VAR-A)) '("amphibians are cool" (INCF VAR-B))) ;;=> T

This is nearly what we want - we just want to cons a #'list at the start. So:

(defmacro build-cond-action-pairs (&rest var)
  (cons 'list `,(to-quoted-pairs `,var)))


(macroexpand-1 '(build-cond-action-pairs "fish are cool" (incf var-a) 
                                         "amphibians are cool" (incf var-b)))
;;=> (LIST '("fish are cool" (INCF VAR-A))
;;=>                          '("amphibians are cool" (INCF VAR-B))) ;
;;=> T
;; That's it! and we can run it without error:
(build-cond-action-pairs "fish are cool" (incf var-a) 
                                         "amphibians are cool" (incf var-b))
;;=> (("fish are cool" (INCF VAR-A)) ("amphibians are cool" (INCF VAR-B)))

Voila! We made it!