【问题标题】:macro to feed a calculated binding list into a 'let'?宏将计算的绑定列表提供给“让”?
【发布时间】:2015-04-12 16:11:17
【问题描述】:

我正在为宏 lambda 列表尝试不同的绑定模型。

编辑:事实上,我的测试宏的 lambda 列表总是(&rest ...)。这意味着我正在“解构”参数列表而不是 lambda 列表。我试图获得一个解决方案,可以将可选与关键参数或rest/body 与关键参数结合起来——这两种组合在 Common Lisp 标准实现中都不起作用。

所以我有不同的函数给我一个绑定列表,这些绑定的语法与 'let' 使用的语法相同。

例如:

(build-bindings ...) => ((first 1) middle (last "three"))

现在我想在我的测试宏中使用一个简单的宏,将这样的列表提供给“让”。

如果我有一个文字列表,这很简单:

(defmacro let-list (_list &rest _body)
  `(let ,_list ,@_body))

(let-list ((a 236)) a) => 236

但这与简单的“让”相同。

我想要的是与生成的列表相同的东西。

例如

(let-list (build-bindings ...)
    (format t "first: ~s~%" first)
    last)

使用(build-bindings ...),在与调用(let-list ...) 相同的词法范围内求值,返回

((first 1) middle (last "three"))

宏的扩展应该是

(let
  ((first 1) middle (last "three"))

  (format t "first: ~s~%" first)
  last)

应该打印1并返回"three"

知道如何实现吗?

编辑(使问题更笼统):

如果我有一个 (symbol value) 对的列表,即 let 对其绑定列表所需的相同语法,例如((one 1) (two 'two) (three "three")),有没有办法编写一个宏来创建符号的词法绑定,并为其&rest/&body 参数提供值?

这似乎是 Joshua 向我指出的一个可能的解决方案:

(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

我还可以轻松地重新排列我的 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

更好的方法可能是:

(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

我目前看不出动态progv绑定是否还有其他问题。

但是 progv 的整个辣酱玉米饼馅包裹在 locally 中,所有的符号都被声明为宏的特殊呼叫 - 由于同样的原因,这又是不可能的 let-list 不起作用:(

可能是一种我不知道的macro-lambda-list destructuring-hook。

我必须研究destructuring-bind 的实现,因为那个宏可以做我想做的事情。也许这会启发我;)

【问题讨论】:

  • 您知道不能在运行时轻松生成绑定列表吗?想象一下编译器看到代码而不执行它。它需要在编译时运行build-bindings...
  • build-bindings 实际上是在编译时执行的,因为它是用于宏内部而不是函数内部,所以以自定义方式“解构”参数所需的所有信息都存在于编译时 -假设我对这个假设没有错。问题是如何将build-binding 的结果插入到运行时let
  • 请展示您希望能够编写的代码的具体示例,以及它应该扩展到的代码。使用简写 (let-list (bindings... ) ...) 并不能告诉我们您实际期望用什么来代替 (bindings... )
  • 我已经阅读了这个问题,但仍然不清楚你想要什么。我们是否应该将(build-bindings ...) 视为返回((first 1) middle (last "three")) 的函数?如果是这样,我认为你想要的不可能以你想要的方式实现。
  • @carpetemporem 是的,但是 build-bindingsarguments 是什么?有什么东西可以让我们在编译时知道变量是firstmiddlelast,它们的值是1、@987654357 @ 和 "three"?如果我们要帮助您解决这个问题,这对我们来说是非常重要的信息。 “有没有办法编写一个宏来创建符号的词法绑定与它的 &rest/&body 参数提供的值?”的答案?绝对是是的,但是您需要能够在宏扩展时间获得这些符号。没有

标签: macros common-lisp


【解决方案1】:

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

(defun build-bindings ()
  '((first 1) middle (last "three")))

(defmacro let-list (bindings &body body)
  `(let ,bindings
     ,@body))

然后您可以尝试执行以下操作:

(let-list (build-bindings)
  (print first))

当然,这是行不通的,因为宏扩展将 (build-bindings) 留在生成的 let 中,处于它不会的位置' t被评估:

CL-USER> (pprint (macroexpand-1 '(let-list (build-bindings)
                                  (print first))))
(LET (BUILD-BINDINGS)
  (PRINT FIRST))

宏观扩张期间的评估

问题是您希望在 macroexpansion 时间获得 build-bindingsresult,而那是 before运行整个代码。现在,在这个例子中,build-bindings 可以在宏扩展时运行,因为它没有对任何参数做任何事情(记得我在评论中问过参数是什么? )。这意味着您实际上可以在宏扩展中eval它:

(defmacro let-list (bindings &body body)
  `(let ,(eval bindings)
     ,@body))

CL-USER> (pprint (macroexpand-1 '(let-list (build-bindings)
                                  (print first))))
(LET ((FIRST 1) MIDDLE (LAST "three"))
  (PRINT FIRST))

现在这将起作用,只要它将 firstmiddlelast 绑定到 1nil“三个”。但是,如果 build-bindings 确实需要一些在宏扩展时不可用的参数,那么您将不走运。首先,它可以接受在宏扩展时可用的参数(例如,常量):

(defun build-bindings (a b &rest cs)
  `((first ',a) (middle ',b) (last ',cs)))

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

您还可以在其中显示一些变量:

(defun build-bindings (x ex y why)
  `((,x ,ex) (,y ,why)))

CL-USER> (pprint (macroexpand-1 '(let-list (build-bindings 'a 'ay 'b 'bee)
                                  (print first))))
(LET ((A AY) (B BEE))
  (PRINT FIRST))

但是,您不能做的是让变量名称从运行时才存在的值中确定。例如,您不能执行以下操作:

(let ((var1 'a)
      (var2 'b))
  (let-list (build-bindings var1 'ay var2 'bee)
    (print first))

因为 (let-list (build-bindings ...) ...) 被宏扩展任何这段代码实际上执行。这意味着当 var1var2 未绑定时,您将尝试评估 (build-bindings var1 'ay var2 'bee)任何值。

Common Lispall 首先进行宏扩展,然后然后 评估代码。这意味着直到运行时才可用的值在宏扩展时可用。

运行时编译(和宏扩展)

现在,尽管我说 Common Lisp 先进行所有的宏扩展,然后评估代码,但上面的代码实际上在宏扩展时使用了 eval,以便更早地获得一些额外的评估。我们也可以朝另一个方向做事;我们可以在运行时使用compile。这意味着我们可以生成一个 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))))

CL-USER> (%dynamic-lambda '((first 1) middle (last "three")) 
                          '((list first middle last)))
;=> (1 NIL "three")

这会编译一个在运行时从主体和绑定列表创建的 lambda 表达式。编写一个宏来消除引用的麻烦并不难:

(defmacro let-list (bindings &body body)
  `(%dynamic-lambda ,bindings ',body))

CL-USER> (let-list '((first 1) middle (last "three")) 
           (list first middle last))
;=> (1 NIL "three")

CL-USER> (macroexpand-1 '(let-list (build-bindings)
                          (list first middle last)))
;=> (%DYNAMIC-LAMBDA (BUILD-BINDINGS) '((LIST FIRST MIDDLE LAST)))

CL-USER> (flet ((build-bindings ()
                  '((first 1) middle (last "three"))))
           (let-list (build-bindings)
             (list first middle last)))
;=> (1 NIL "three")

这为您提供了运行时创建的绑定列表中的真正词法变量。当然,因为编译是在运行时进行的,所以您无法访问词法环境。这意味着您正在编译为函数的主体无法访问“周围的”词法范围。例如:

CL-USER> (let ((x 3))
           (let-list '((y 4))
             (list x y)))
; Evaluation aborted on #<UNBOUND-VARIABLE X {1005B6C2B3}>.

使用 PROGV 和特殊变量

如果您不需要词法变量,但可以使用特殊(即动态范围)变量,则可以在运行时使用 progv 建立绑定。这看起来像:

(progv '(a b c) '(1 2 3)
  (list c b a))
;;=> (3 2 1)

如果运行它,您可能会收到一些警告,因为在编译表单时,无法知道 a、b 和 c 应该是特殊变量。不过,您可以使用 locally 添加一些特殊声明:

(progv '(a b c) '(1 2 3)
  (locally
      (declare (special a b c))
    (list c b a)))
;;=> (3 2 1)

当然,如果您这样做,那么您必须提前知道变量,这正是您一开始就试图避免的。但是,如果您愿意提前知道变量的名称(your comments 似乎您可以接受),那么您实际上可以使用词法变量。

具有的词法变量在运行时计算

如果您愿意说明变量是什么,但仍想在运行时动态计算它们的值,那么您可以相对轻松地做到这一点。首先,让我们编写直接版本(没有宏):

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

现在,编写一个宏化版本并不难。这个版本并不完美,(例如,名称可能存在卫生问题,并且主体中的声明将不起作用(因为在某些东西之后会拼接主体)。不过,这是一个开始:

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

(computed-let (a b c) '((a 1) b (c 3))
  (list a b c))
;;=> (1 NIL 3)

使这个更清晰的一种方法是完全避免赋值,而计算值直接为绑定提供值:

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

此版本创建一个 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)

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

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

【讨论】:

  • 谢谢约书亚。我读了很多东西,我尝试了很多东西,我现在看到,CL 不能做我想做的事。为了简单起见:我想评估宏的某些参数,而不是传递源代码。这将需要某种运行时宏扩展模型。尽管如此,我对“可扩展语言”只有编译时宏扩展工具感到非常失望。
  • @carpetemporem 好吧,您可以在运行时绑定变量。您将使用progv,它采用符号列表和值列表。运行时宏扩展的问题在于,您无法有意义地编译代码,因为您永远不知道要编译的代码会是什么。 (例如,对 middle 的引用应该是词法变量引用还是动态变量引用?你不知道你是否实际上没有代码。)
  • @carpetemporem 你是对的,尽管 Common Lisp 不能,但设计不同的语言(例如,只允许解释代码)可以做到这一点。您当然可以使用 Common Lisp 实现该语言。编写解决方法也可能更容易。
  • @carpetemporem 实际上,我只是想到了一个您可能感兴趣的选项......让我写下来......
  • @carpetemporem 是的,请查看更新后的答案。我的答案的第一个版本描述了如何在早期(即在宏扩展阶段)进行评估,但完全忽略了您可以在运行时调用编译器(反过来,但在这里不是必需的宏扩展器)这一事实。如果你这样做,那么你可以动态生成词法变量(但代价是失去对周围代码的一些词法访问)。这可能是一个更有帮助的选择。
【解决方案2】:

有点间接:

一种解决方案,用于将可选与关键参数或 带有关键参数的休息/正文

您是否考虑过为关键字使用子列表的并非完全不常见的范例?

例如

 (defmacro something (&key (first 1) second) &body body) ... )

或者,来自亚历山大的实际用途:

 (defmacro with-output-to-file ((stream-name file-name
                                 &rest args
                                 &key (direction nil direction-p)
                                 &allow-other-keys)
                                &body body)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-03-11
    • 2016-11-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-05-19
    • 1970-01-01
    相关资源
    最近更新 更多