宏将计算出的绑定列表提供给“让”?

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因为该宏做了我想做的事情。也许这会让我有所启发;)

Jos*_*lor 5

所以第一次(不正确的)尝试看起来像这样:

(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)

现在这将起作用,因为它将firstmiddlelast 分别绑定到1nil"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 ...) ...)任何这些代码实际执行之前被宏展开。这意味着当var1var2未绑定到任何值时,您将尝试评估(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建立绑定。那看起来像:

(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 或 DESTRUCTURING-BIND

因为我正在对参数进行一些“解构”(以稍微不同的方式),所以我知道哪些参数必须存在,或者在缺少可选参数和关键参数的情况下具有哪些默认值。因此,在第一步中,我会得到一个值列表和一个标志,即可选参数或关键参数是否存在或默认。在第二步中,我想将这些值和/或存在/默认标志绑定到局部变量以对它们进行一些工作

这实际上听起来好像您可以通过使用 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)

所以,如果你只是让你的函数返回绑定作为关键字参数列表,那么在解构或函数应用程序中你可以自动绑定相应的变量,分配默认值,并检查是否提供了非默认值。