Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Passing a list to a macro correctly in Common Lisp

I wrote a macro to do multiple nested loops. I understand that there are other facilities to do this, but I am trying to learn how to write macros, and this seemed like a good use-case. It works too (kind of):

(defmacro dotimes-nested (nlist fn)
  (let ((index-symbs nil)
        (nlist (second nlist))) ;remove quote from the beginning of nlist
    (labels 
      ((rec (nlist)
            (cond ((null nlist) nil)
                  ((null (cdr nlist))
                   (let ((g (gensym)))
                     (push g index-symbs)
                     `(dotimes (,g ,(car nlist) ,g)
                        (funcall ,fn ,@(reverse index-symbs)))))
                  (t (let ((h (gensym)))
                       (push h index-symbs)
                       `(dotimes (,h ,(car nlist) ,h)
                          ,(rec (cdr nlist))))))))
      (rec nlist))))

running:

(macroexpand-1 '(dotimes-nested '(2 3 5)
                #'(lambda (x y z) (format t "~A, ~A, ~A~%" x y z))))

outputs:

(DOTIMES (#:G731 2 #:G731)
  (DOTIMES (#:G732 3 #:G732)
    (DOTIMES (#:G733 5 #:G733)
      (FUNCALL #'(LAMBDA (X Y Z) (FORMAT T "~A, ~A, ~A~%" X Y Z)) #:G731 #:G732
               #:G733))))

and calling the macro like this:

(dotimes-nested '(2 3 5)
                #'(lambda (x y z) (format t "~A, ~A, ~A~%" x y z)))

returns correctly what I expect:

0, 0, 0
0, 0, 1
0, 0, 2
0, 0, 3
0, 0, 4
0, 1, 0
0, 1, 1
0, 1, 2
0, 1, 3
0, 1, 4
0, 2, 0
0, 2, 1
0, 2, 2
0, 2, 3
0, 2, 4
1, 0, 0
1, 0, 1
1, 0, 2
1, 0, 3
1, 0, 4
1, 1, 0
1, 1, 1
1, 1, 2
1, 1, 3
1, 1, 4
1, 2, 0
1, 2, 1
1, 2, 2
1, 2, 3
1, 2, 4
2

However, if I call it like this:

(let ((dims '(3 4)))
    (dotimes-nested dims
                #'(lambda (x y) (format t "~A, ~A~%" x y))))

I get an error:

; in: LET ((DIMS '(3 4)))
;     (DOTIMES-NESTED DIMS #'(LAMBDA (X Y) (FORMAT T "~A, ~A~%" X Y)))
; 
; caught ERROR:
;   during macroexpansion of (DOTIMES-NESTED DIMS #'(LAMBDA # #)). Use
;   *BREAK-ON-SIGNALS* to intercept.
;   
;    The value DIMS is not of type LIST.

;     (LET ((DIMS '(3 4)))
;       (DOTIMES-NESTED DIMS #'(LAMBDA (X Y) (FORMAT T "~A, ~A~%" X Y))))
; 
; caught STYLE-WARNING:
;   The variable DIMS is defined but never used.
; 
; compilation unit finished
;   caught 1 ERROR condition
;   caught 1 STYLE-WARNING condition

How do I pass in a variable that isn't interpreted as a symbol but as it's value? Should I evaluate it within the macro using , ? But I can't figure out how. Also how would both situations work, a: calling the macro with a literal '(3 4), and b: passing in a symbol bound to the list '(3 4)? Would I need separate macros for each?

Finally, I have a secondary question. When I do pass in a literal for nlist, I have to use (second nlist) to get to the list values. I understand this is because '(3 4) gets expanded to (quote (3 4)) before being sent to the macro. Is this a correct assumption? And if so, is this general practice, i.e. - to use the second value of a literal list passed to a macro?

like image 494
Capstone Avatar asked Dec 24 '22 01:12

Capstone


2 Answers

Macros are expanded before the code is compiled or run. The variable DIMS however only exists when the code is run. The arguments given to a macro are literal data, so when you do

(dotimes-nested dims ...)

DIMS is not a reference to the variable (which doesn't exist yet), but just a symbol.

It's also unnecessary to quote the list when calling a macro since it's literal data anyway. So you should just call it like (dotimes-nested (2 3 4) ...) (and no need to remove anything in the macro).

If you do need to be able to use (runtime) variables for the dimensions, you should use a regular function instead of a macro. Something like:

(defun dotimes-nested (nlist function)
  (labels ((rec (nlist args)
             (if (endp nlist)
                 (apply function (reverse args))
                 (destructuring-bind (first . rest) nlist
                   (dotimes (i first)
                     (rec rest (cons i args)))))))
    (rec nlist nil)))

CL-USER> (let ((dims '(3 4)))
           (dotimes-nested dims
                           #'(lambda (x y) (format t "~A, ~A~%" x y))))
0, 0
0, 1
0, 2
0, 3
1, 0
1, 1
1, 2
1, 3
2, 0
2, 1
2, 2
2, 3
NIL
like image 195
jkiiski Avatar answered Jan 11 '23 15:01

jkiiski


In your use of the macro I spot that you had quotes the literal list and in your implementation you actually apply second with the comment ; remove quote from the beginning of nlist

A macro takes the code input and applies the macro function to those unevaluated expressions, that is purely data just refering to the surface syntax, with the result being new code. This code can then be replaced with the use of the macro at every place it's used.

Expansion happens once. Usually all macros in a function are expanded when the function is stored and when using the function there are no traces of the macro ever existed.

When you have something like:

(dotimes-nested dims
                #'(lambda (x y) (format t "~A, ~A~%" x y))))

The macro gets the symbol dims. Your resulting code should then have dims so that ebentually evaluates to a list in runtime since you have no idea what it might be at the time the macro is expanding.

I would have done it like this instead:

(dotimes* ((a 3) (b 2) (c 10))
  (format t "~A, ~A, ~A~%" a b c))

(defmacro dotimes* ((&rest binding-initials) &body body)
  (loop :for (binding initial) :in (reverse binding-initials)
        :for result := `(dotimes (,binding ,initial nil) ,@body) 
                    :then `(dotimes (,binding ,initial nil) ,result)
        :finally (return result)))

The nice thing about this is that you can use variables here:

(defparameter *x* 10) 
(defparameter *y* 10)
(dotimes* ((x *x*) (y *y*))
  (format t "(~a,~a)~%" x y))

If you have a dynamic number of variables then making it a function, like @kiiski's answer, would be a better match.

like image 41
Sylwester Avatar answered Jan 11 '23 15:01

Sylwester