car*_*rem 2 macros common-lisp
我正在为宏 lambda 列表尝试不同的绑定模型。
编辑:事实上,我的测试宏的 lambda 列表总是(&rest ...). 这意味着我正在“解构”参数列表而不是 lambda 列表。我尝试获得一个解决方案,该解决方案适用于将 optional 与关键参数或 rest/body 与关键参数相结合——这两种组合在 Common Lisp 标准实现中都不起作用。
所以我有不同的函数给我一个绑定列表,这些绑定的语法与“let”使用的语法相同。
例如:
(build-bindings ...) => ((first 1) middle (last "three"))
Run Code Online (Sandbox Code Playgroud)
现在我想在我的测试宏中使用一个简单的宏,将这样的列表提供给“让”。
如果我有一个文字列表,这很简单:
(defmacro let-list (_list &rest _body)
`(let ,_list ,@_body))
(let-list ((a 236)) a) => 236
Run Code Online (Sandbox Code Playgroud)
但这与简单的“让”相同。
我想要的与生成的列表相同。
所以例如
(let-list (build-bindings ...)
(format t "first: ~s~%" first)
last)
Run Code Online (Sandbox Code Playgroud)
with (build-bindings ...),在与调用相同的词法范围内求值(let-list ...),返回
((first 1) middle (last "three"))
Run Code Online (Sandbox Code Playgroud)
宏的扩展应该是
(let
((first 1) middle (last "three"))
(format t "first: ~s~%" first)
last)
Run Code Online (Sandbox Code Playgroud)
并且应该打印1并返回"three"。
知道如何做到这一点吗?
编辑(使问题更笼统):
如果我有一个(symbol value)对的列表,即let需要它的绑定列表的相同语法,例如((one 1) (two 'two) (three "three")),有没有办法编写一个宏来创建符号的词法绑定,并为其&rest/&body参数提供值?
这似乎是约书亚向我指出的一个可能的解决方案:
(let ((list_ '((x 23) (y 6) z)))
(let
((symbols_(loop for item_ in list_
collect (if (listp item_) (car item_) item_)))
(values_ (loop for item_ in list_
collect (if (listp item_) (cadr item_) nil))))
(progv symbols_ values_
(format t "x ~s, y ~s, z ~s~%" x y z))))
evaluates to:
;Compiler warnings :
; In an anonymous lambda form: Undeclared free variable X
; In an anonymous lambda form: Undeclared free variable Y
; In an anonymous lambda form: Undeclared free variable Z
x 23, y 6, z NIL
Run Code Online (Sandbox Code Playgroud)
我还可以轻松地重新排列我的build-bindings函数以返回所需的两个列表。
一个问题是,如果变量从未被声明为特殊的,编译器就会发出警告。
另一个问题是,如果动态绑定变量也用于周围的词法绑定,它们会被词法绑定遮蔽——如果它们从未被声明为特殊的:
(let ((x 47) (y 11) (z 0))
(let ((list_ '((x 23) (y 6) z)))
(let
((symbols_(loop for item_ in list_
collect (if (listp item_) (car item_) item_)))
(values_ (loop for item_ in list_
collect (if (listp item_) (cadr item_) nil))))
(progv symbols_ values_
(format t "x ~s, y ~s, z ~s~%" x y z)))))
evaluates to:
x 47, y 11, z 0
Run Code Online (Sandbox Code Playgroud)
更好的方法可能是:
(let ((x 47) (y 11) (z 0))
(locally
(declare (special x y))
(let ((list_ '((x 23) (y 6) z)))
(let
((symbols_(loop for item_ in list_
collect (if (listp item_) (car item_) item_)))
(values_ (loop for item_ in list_
collect (if (listp item_) (cadr item_) nil))))
(progv symbols_ values_
(format t "x ~s, y ~s, z ~s~%" x y z))))))
evaluates to:
;Compiler warnings about unused lexical variables skipped
x 23, y 6, z NIL
Run Code Online (Sandbox Code Playgroud)
我目前看不到动态progv绑定是否存在其他问题。
但是整个 enchiladaprogv包裹在locally所有符号中再次声明为宏的特殊哭声 - 由于相同的原因let-list,这又是不可能的 :(
可能性是一种我不知道的宏 lambda 列表解构钩子。
我必须研究 的实现,destructuring-bind因为该宏做了我想做的事情。也许这会让我有所启发;)
所以第一次(不正确的)尝试看起来像这样:
(defun build-bindings ()
'((first 1) middle (last "three")))
(defmacro let-list (bindings &body body)
`(let ,bindings
,@body))
Run Code Online (Sandbox Code Playgroud)
然后你可以尝试做这样的事情:
(let-list (build-bindings)
(print first))
Run Code Online (Sandbox Code Playgroud)
这当然行不通,因为宏扩展将结果let 中的表单(构建绑定)留在了不会被评估的位置:
CL-USER> (pprint (macroexpand-1 '(let-list (build-bindings)
(print first))))
(LET (BUILD-BINDINGS)
(PRINT FIRST))
Run Code Online (Sandbox Code Playgroud)
问题是您希望在宏扩展时获得构建绑定的结果,而那是在整个代码运行之前。现在,在这个例子中,构建绑定可以在宏扩展时运行,因为它没有对任何参数做任何事情(记得我在评论中问过参数是什么?)。这意味着,你实际上可以EVAL在宏扩展它:
(defmacro let-list (bindings &body body)
`(let ,(eval bindings)
,@body))
Run Code Online (Sandbox Code Playgroud)
CL-USER> (pprint (macroexpand-1 '(let-list (build-bindings)
(print first))))
(LET ((FIRST 1) MIDDLE (LAST "three"))
(PRINT FIRST))
Run Code Online (Sandbox Code Playgroud)
现在这将起作用,因为它将first、middle和last 分别绑定到1、nil和"three"。但是,如果构建绑定实际上需要一些在宏扩展时不可用的参数,那么您将不走运。首先,它可以采用宏展开时可用的参数(例如,常量):
(defun build-bindings (a b &rest cs)
`((first ',a) (middle ',b) (last ',cs)))
Run Code Online (Sandbox Code Playgroud)
CL-USER> (pprint (macroexpand-1 '(let-list (build-bindings 1 2 3 4 5)
(print first))))
(LET ((FIRST '1) (MIDDLE '2) (LAST '(3 4 5)))
(PRINT FIRST))
Run Code Online (Sandbox Code Playgroud)
您还可以在其中显示一些变量:
(defun build-bindings (x ex y why)
`((,x ,ex) (,y ,why)))
Run Code Online (Sandbox Code Playgroud)
CL-USER> (pprint (macroexpand-1 '(let-list (build-bindings 'a 'ay 'b 'bee)
(print first))))
(LET ((A AY) (B BEE))
(PRINT FIRST))
Run Code Online (Sandbox Code Playgroud)
但是,您不能做的是根据直到运行时才存在的值来确定变量名称。例如,您不能执行以下操作:
(let ((var1 'a)
(var2 'b))
(let-list (build-bindings var1 'ay var2 'bee)
(print first))
Run Code Online (Sandbox Code Playgroud)
因为(let-list (build-bindings ...) ...)在任何这些代码实际执行之前被宏展开。这意味着当var1和var2未绑定到任何值时,您将尝试评估(build-bindings var1 'ay var2 'bee)。
Common Lisp 首先完成所有的宏扩展,然后评估代码。这意味着在运行时才可用的值在宏扩展时不可用。
现在,尽管我说 Common Lisp 先做所有的宏扩展,然后计算代码,但上面的代码实际上在宏扩展中使用了eval来提前获得一些额外的计算。我们也可以在另一个方向上做事情;我们可以在运行时使用编译。这意味着我们可以生成一个 lambda 函数并根据运行时提供的代码(例如,变量名)编译它。我们实际上可以在不使用宏的情况下做到这一点:
(defun %dynamic-lambda (bindings body)
(flet ((to-list (x) (if (listp x) x (list x))))
(let* ((bindings (mapcar #'to-list bindings))
(vars (mapcar #'first bindings))
(vals (mapcar #'second bindings)))
(apply (compile nil `(lambda ,vars ,@body)) vals))))
Run Code Online (Sandbox Code Playgroud)
CL-USER> (%dynamic-lambda '((first 1) middle (last "three"))
'((list first middle last)))
;=> (1 NIL "three")
Run Code Online (Sandbox Code Playgroud)
这将编译在运行时从主体和绑定列表创建的 lambda 表达式。编写一个宏来消除引用的麻烦并不难:
(defmacro let-list (bindings &body body)
`(%dynamic-lambda ,bindings ',body))
Run Code Online (Sandbox Code Playgroud)
CL-USER> (let-list '((first 1) middle (last "three"))
(list first middle last))
;=> (1 NIL "three")
Run Code Online (Sandbox Code Playgroud)
CL-USER> (macroexpand-1 '(let-list (build-bindings)
(list first middle last)))
;=> (%DYNAMIC-LAMBDA (BUILD-BINDINGS) '((LIST FIRST MIDDLE LAST)))
Run Code Online (Sandbox Code Playgroud)
CL-USER> (flet ((build-bindings ()
'((first 1) middle (last "three"))))
(let-list (build-bindings)
(list first middle last)))
;=> (1 NIL "three")
Run Code Online (Sandbox Code Playgroud)
这从运行时创建的绑定列表中为您提供真正的词法变量。当然,因为编译是在运行时进行的,所以您无法访问词法环境。这意味着您编译成函数的主体无法访问“周围”词法范围。例如:
CL-USER> (let ((x 3))
(let-list '((y 4))
(list x y)))
; Evaluation aborted on #<UNBOUND-VARIABLE X {1005B6C2B3}>.
Run Code Online (Sandbox Code Playgroud)
如果您不需要词法变量,但可以使用特殊(即动态范围)变量代替,您可以在运行时使用progv建立绑定。那看起来像:
(progv '(a b c) '(1 2 3)
(list c b a))
;;=> (3 2 1)
Run Code Online (Sandbox Code Playgroud)
如果运行它,您可能会收到一些警告,因为在编译表单时,无法知道 a、b 和 c 应该是特殊变量。不过,您可以在本地使用来添加一些特殊声明:
(progv '(a b c) '(1 2 3)
(locally
(declare (special a b c))
(list c b a)))
;;=> (3 2 1)
Run Code Online (Sandbox Code Playgroud)
当然,如果你这样做,那么你必须提前知道变量,这正是你首先要避免的。但是,如果您愿意提前知道变量的名称(并且您的评论似乎可以接受),那么您实际上可以使用词法变量。
如果您愿意说明变量将是什么,但仍希望在运行时动态计算它们的值,则可以相对轻松地做到这一点。首先,让我们编写直接版本(没有宏):
;; Declare three lexical variables, a, b, and c.
(let (a b c)
;; Iterate through a list of bindings (as for LET)
;; and based on the name in the binding, assign the
;; corresponding value to the lexical variable that
;; is identified by the same symbol in the source:
(dolist (binding '((c 3) (a 1) b))
(destructuring-bind (var &optional value)
(if (listp binding) binding (list binding))
(ecase var
(a (setf a value))
(b (setf b value))
(c (setf c value)))))
;; Do something with the lexical variables:
(list a b c))
;;=> (1 NIL 3)
Run Code Online (Sandbox Code Playgroud)
现在,编写一个宏化版本并不太难。这个版本并不完美,(例如,名称可能存在卫生问题,并且主体中的声明不起作用(因为主体是在一些东西之后拼接起来的)。不过,这是一个开始:
(defmacro computed-let (variables bindings &body body)
(let ((assign (gensym (string '#:assign-))))
`(let ,variables
(flet ((,assign (binding)
(destructuring-bind (variable &optional value)
(if (listp binding) binding (list binding))
(ecase variable
,@(mapcar (lambda (variable)
`(,variable (setf ,variable value)))
variables)))))
(map nil #',assign ,bindings))
,@body)))
Run Code Online (Sandbox Code Playgroud)
(computed-let (a b c) '((a 1) b (c 3))
(list a b c))
;;=> (1 NIL 3)
Run Code Online (Sandbox Code Playgroud)
使这个更清晰的一种方法是完全避免赋值,并且计算值直接为绑定提供值:
(defmacro computed-let (variables bindings &body body)
(let ((values (gensym (string '#:values-)))
(variable (gensym (string '#:variable-))))
`(apply #'(lambda ,variables ,@body)
(let ((,values (mapcar #'to-list ,bindings)))
(mapcar (lambda (,variable)
(second (find ,variable ,values :key 'first)))
',variables)))))
Run Code Online (Sandbox Code Playgroud)
此版本创建一个 lambda 函数,其中参数是指定的变量,主体是提供的主体(因此主体中的声明位于适当的位置),然后将其应用于从计算结果中提取的值列表绑定。
因为我正在对参数进行一些“解构”(以稍微不同的方式),所以我知道哪些参数必须存在,或者在缺少可选参数和关键参数的情况下具有哪些默认值。因此,在第一步中,我会得到一个值列表和一个标志,即可选参数或关键参数是否存在或默认。在第二步中,我想将这些值和/或存在/默认标志绑定到局部变量以对它们进行一些工作
这实际上听起来好像您可以通过使用 lambda 函数或使用关键字参数进行解构绑定来完成您需要的操作。首先,请注意您可以使用任何符号作为关键字参数指示符。例如:
(apply (lambda (&key
((b bee) 'default-bee b?)
((c see) 'default-see c?))
(list bee b? see c?))
'(b 42))
;;=> (42 T DEFAULT-SEE NIL)
Run Code Online (Sandbox Code Playgroud)
(destructuring-bind (&key ((b bee) 'default-bee b?)
((c see) 'default-see c?))
'(b 42)
(list bee b? see c?))
;;=> (42 T DEFAULT-SEE NIL)
Run Code Online (Sandbox Code Playgroud)
所以,如果你只是让你的函数返回绑定作为关键字参数列表,那么在解构或函数应用程序中你可以自动绑定相应的变量,分配默认值,并检查是否提供了非默认值。
| 归档时间: |
|
| 查看次数: |
558 次 |
| 最近记录: |