我写了一个宏来做多个嵌套循环。我知道还有其他设施可以做到这一点,但我正在努力学习如何编写宏,这似乎是一个很好的用例。它也有效(有点):
(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))))
运行:
(macroexpand-1 '(dotimes-nested '(2 3 5)
#'(lambda (x y z) (format t "~A, ~A, ~A~%" x y z))))
输出:
(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))))
并像这样调用宏:
(dotimes-nested '(2 3 5)
#'(lambda (x y z) (format t "~A, ~A, ~A~%" x y z)))
正确地返回我的期望:
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
但是,如果我这样称呼它:
(let ((dims '(3 4)))
(dotimes-nested dims
#'(lambda (x y) (format t "~A, ~A~%" x y))))
我收到错误:
; 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
如何传入一个未被解释为符号的变量,但是它的值是什么?我应该使用宏来评估它吗?但我无法弄清楚如何。这两种情况如何运作,a:用文字'(3 4)调用宏,b:传入绑定到列表的符号'(3 4)?我需要为每个宏分别使用单独的宏吗?
最后,我有一个次要问题。
当我传入nlist的文字时,我必须使用(second nlist)
来获取列表值。我理解这是因为'(3 4)
在发送到宏之前会扩展到(quote (3 4))
。这是正确的假设吗?如果是这样,这是一般的做法,即 - 使用传递给宏的文字列表的第二个值吗?
答案 0 :(得分:5)
在编译或运行代码之前会扩展宏。但是,变量DIMS
仅在代码运行时存在。给宏的参数是文字数据,所以当你做
(dotimes-nested dims ...)
DIMS
不是对变量的引用(它还不存在),而只是一个符号。
在调用宏时也不必引用列表,因为它无论如何都是文字数据。所以你应该像(dotimes-nested (2 3 4) ...)
一样调用它(并且不需要删除宏中的任何内容)。
如果确实需要能够为维度使用(运行时)变量,则应使用常规函数而不是宏。类似的东西:
(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
答案 1 :(得分:3)
在您使用宏时,我发现您引用了文字列表,并且在您的实现中,您实际应用了second
评论; remove quote from the beginning of nlist
宏接受代码输入并将宏函数应用于那些未评估的表达式,这纯粹是仅仅引用表面语法的数据,结果是新代码。然后可以在使用它的每个地方使用宏来替换此代码。
扩张一次。通常,当存储函数时,函数中的所有宏都会被扩展,并且当使用该函数时,不存在宏的痕迹。
当你有类似的东西时:
(dotimes-nested dims
#'(lambda (x y) (format t "~A, ~A~%" x y))))
宏获取符号dims
。然后,生成的代码应该具有dims
,以便在运行时根据您不知道宏扩展时的内容进行计算。
我会这样做:
(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)))
关于这一点的好处是你可以在这里使用变量:
(defparameter *x* 10)
(defparameter *y* 10)
(dotimes* ((x *x*) (y *y*))
(format t "(~a,~a)~%" x y))
如果你有一个动态数量的变量然后使它成为一个函数,比如@ kiiski的答案,那将是一个更好的匹配。