【问题标题】:Common lisp macro not calling function通用 lisp 宏不调用函数
【发布时间】:2021-10-13 10:43:42
【问题描述】:

我正在处理一个复杂的宏并且遇到了障碍。

(defmacro for-each-hashtable-band (body vars on &optional counter name)
  `(block o
     (with-hash-table-iterator (next-entry ,on)
       (destructuring-bind
            ,(apply #'append vars)
            (let ((current-band (list ,@(mapcar #'not (apply #'append vars)))))
              (for (i 1 ,(length (apply #'append vars)) 2)
                (multiple-value-bind
                      (succ k v) (next-entry)
                  (if succ
                      (progn
                        (setf (nth i current-band) k)
                        (setf (nth (+ 1 i) current-band) v))
                      (return-from o nil))))
              current-band)
          ,@body))))

我收到“评估在 # 上中止” 我不明白为什么下一个条目似乎对我创建的宏不可见。

我已经尝试将其简化为一个可复制的小示例,但如果没有我创建的宏,我找不到一个最小的场景,无论我尝试什么,除了这个场景之外,下一个条目将是不可见的,我总是设法找到一种在我的其他示例中调用 next-entry 的方法,所以我很难理解为什么我不能让它在这里工作

我已经测试了我创建的 for 宏,它似乎在大多数情况下都可以正常工作,但由于某种原因,它看不到这个 next-entry 变量。如何让它可见?

【问题讨论】:

  • ,(next-entry) 在宏展开时求值。但是在宏扩展时没有这样的运算符。该宏正在尝试创建代码,然后在宏扩展之后通过 with-hash-table-iterator 宏具有这样的运算符。为时已晚。如果你想在宏展开的时候调用一个操作符,那么这个操作符需要在那个时候定义,而不是在生成的代码中。
  • ,(next-entry) 应该是 (next-entry) 没有取消引用与开头定义 next-entry 的表达式处于同一级别。
  • 像往常一样编写宏时,您应该问自己扩展后的代码到底应该是什么。并通过macroexpand-1 进行检查。然后,您将看到大部分错误。 (macroexpand-1 '(here the macro-call with some arguments)
  • ,(next-entry)(next-entry) 都产生相同的错误,问题是代码似乎无法访问 defun、flet、labels 等引入的函数。只有那些引入的函数通过 defvar、let、let* 等
  • (将,(next-entry) 修复为(next-entry)) 的编辑答案,尽管发生了完全相同的问题)

标签: common-lisp lisp-macros


【解决方案1】:

在您的代码中,宏在多个位置以受variable capture (pdf) 约束的方式生成绑定。

(defmacro for-each-hashtable-band (body vars on &optional counter name)
  `(block o ;; VARIABLE CAPTURE
     (with-hash-table-iterator (next-entry ,on) ;; VARIABLE CAPTURE
       (destructuring-bind ,(apply #'append vars)
           (let ((current-band ;;; VARIABLE CAPTURE
                   (list ,@(mapcar #'not (apply #'append vars)))))
             (for
                 (i ;;; VARIABLE CAPTURE
                  1 ,(length (apply #'append vars)) 2)
                 (multiple-value-bind (succ k v) ;;; VARIABLE CAPTURE
                     ,(next-entry) ;;; WRONG EVALUATION TIME
                   (if succ
                       (progn
                         (setf (nth i current-band) k)
                         (setf (nth (+ 1 i) current-band) v))
                       (return-from o nil))))
             current-band)
         ,@body))))

这种捕获的简化示例是:

`(let ((x 0)) ,@body)

在上面,引入了x 变量,但如果代码在x 已经绑定的上下文中展开,那么body 将无法引用之前的x 绑定并且将始终请参阅x 绑定为零(您通常不希望这种行为)。

改写一个函数

与其为此编写一个大宏,不如先尝试了解您想要实现的目标,然后编写一个高阶函数,即。调用用户提供的函数的函数。

如果我理解正确,您的函数将按 bands 个条目遍历哈希表。我假设vars 包含(key value) 符号对的列表,例如((k1 v1) (k2 v2))。然后,body 作用于带中的所有键/值对。

在下面的代码中,函数map-each-hashtable-band 接受一个函数,一个哈希表,而不是vars,它接受一个大小,带的宽度(对的数量)。

请注意,在您的代码中,您只有一个循环,它使用哈希表迭代器构建了一个带。但是,由于宏被命名为for-each-hashtable-band,我假设您还想遍历所有波段。宏 with-hash-table-iterator 提供了一个迭代器,但它本身并不循环。这就是为什么我在这里有两个循环。

(defun map-each-hashtable-band (function hash-table band-size)
  (with-hash-table-iterator (next-entry hash-table)
    (loop :named outer-loop :do
      (loop
        :with key and value and next-p
        :repeat band-size
        :do (multiple-value-setq (next-p key value) (next-entry))
        :while next-p
        :collect key into current-band
        :collect value into current-band
        :finally (progn
                   (when current-band
                     (apply function current-band))
                   (unless next-p
                     (return-from outer-loop)))))))

例如:

(map-each-hashtable-band (lambda (&rest band) (print `(:band ,band)))
                         (alexandria:plist-hash-table
                          '(:a 0 :b 1 :c 2 :d 3 :e 4 :f 5 :g 6))
                         2)

注意。对哈希表的迭代以任意顺序发生,不能保证您会以任何特定顺序看到条目,这取决于实现。

使用我当前版本的 SBCL,会打印以下内容:

(:BAND (:A 0 :B 1)) 
(:BAND (:C 2 :D 3)) 
(:BAND (:E 4 :F 5)) 
(:BAND (:G 6)) 

将函数包装在宏中

前面的函数可能不是你想要的行为,所以你需要适应你的需要,但是一旦它做了你想要的,你可以在它周围包装一个宏。

(defmacro for-each-hashtable-band (vars hash-table &body body)
  `(map-each-hashtable-band (lambda ,(apply #'append vars) ,@body)
                            ,hash-table
                            ,(length vars)))

例如:

(let ((test (alexandria:plist-hash-table '(:a 0 :b 1 :c 2 :d 3 :e 4 :f 5))))
  (for-each-hashtable-band ((k1 v1) (k2 v2)) test
    (format t "~a -> ~a && ~a -> ~a ~%" k1 v1 k2 v2)))

打印出来:

A -> 0 && B -> 1 
C -> 2 && D -> 3 
E -> 4 && F -> 5 

仅宏观解决方案,以确保完整性

如果你只想有一个宏,你可以先在宏中内联上述函数的主体,你不再需要使用apply,而是需要围绕身体,像你一样使用destructuring-bind。初稿将简单如下,但请注意,这不是一个适当的解决方案:

(defmacro for-each-hashtable-band (vars hash-table &body body)
  (let ((band-size (length vars)))
    `(with-hash-table-iterator (next-entry ,hash-table)
       (loop :named outer-loop :do
         (loop
           :with key and value and next-p
           :repeat ,band-size
           :do (multiple-value-setq (next-p key value) (next-entry))
           :while next-p
           :collect key into current-band
           :collect value into current-band
           :finally (progn
                      (when current-band
                        (destructuring-bind ,(apply #'append vars) current-band
                          ,@body))
                      (unless next-p
                        (return-from outer-loop))))))))

为了避免宏的变量捕获问题,您引入的每个临时变量都必须以在您扩展代码的任何上下文中不能存在的符号命名。因此,我们首先取消引用所有变量,使宏定义无法编译:

(defmacro for-each-hashtable-band (vars hash-table &body body)
  (let ((band-size (length vars)))
    `(with-hash-table-iterator (,next-entry ,hash-table)
       (loop :named ,outer-loop :do
         (loop
           :with ,key and ,value and ,next-p
           :repeat ,band-size
           :do (multiple-value-setq (,next-p ,key ,value) (,next-entry))
           :while ,next-p
           :collect ,key into ,current-band
           :collect ,value into ,current-band
           :finally (progn
                      (when ,current-band
                        (destructuring-bind ,(apply #'append vars) ,current-band
                          ,@body))
                      (unless ,next-p
                        (return-from ,outer-loop))))))))

在编译宏的时候,宏应该在代码中注入符号,但是这里我们有一个编译错误说未定义的变量

;; undefined variables: CURRENT-BAND KEY NEXT-ENTRY NEXT-P OUTER-LOOP VALUE

所以现在,这些变量应该是新鲜的符号:

(defmacro for-each-hashtable-band (vars hash-table &body body)
  (let ((band-size (length vars)))
    (let ((current-band (gensym))
          (key (gensym))
          (next-entry (gensym))
          (next-p (gensym))
          (outer-loop (gensym))
          (value (gensym)))
      `(with-hash-table-iterator (,next-entry ,hash-table)
         (loop :named ,outer-loop :do
           (loop
             :with ,key and ,value and ,next-p
             :repeat ,band-size
             :do (multiple-value-setq (,next-p ,key ,value) (,next-entry))
             :while ,next-p
             :collect ,key into ,current-band
             :collect ,value into ,current-band
             :finally (progn
                        (when ,current-band
                          (destructuring-bind ,(apply #'append vars) ,current-band
                            ,@body))
                        (unless ,next-p
                          (return-from ,outer-loop)))))))))

上面的内容有点冗长,但您可以简化一下。 以下是之前的 for-each-hashtable-band 示例通过这个新宏扩展到

(with-hash-table-iterator (#:g1576 test)
  (loop :named #:g1578
        :do (loop :with #:g1575
                  and #:g1579
                  and #:g1577
                  :repeat 2
                  :do (multiple-value-setq (#:g1577 #:g1575 #:g1579) (#:g1576))
                  :while #:g1577
                  :collect #:g1575 into #:g1574
                  :collect #:g1579 into #:g1574
                  :finally (progn
                            (when #:g1574
                              (destructuring-bind
                                  (k1 v1 k2 v2)
                                  #:g1574
                                (format t "~a -> ~a && ~a -> ~a ~%" k1 v1 k2
                                        v2)))
                            (unless #:g1577 (return-from #:g1578))))))

每次展开它时,#:gXXXX 变量都是不同的,并且不可能隐藏现有的绑定,因此例如,body 可以使用命名为 current-bandvalue 的变量而不会破坏展开的代码。

【讨论】:

  • 那么实际上没有办法用宏来做到这一点吗?我对我的原始代码进行了一些不同的更改,但我认为它失败的原因是 flet、标签等引入的函数由于某种原因对反引号或 eval 不可见,这包括由 with-hash- 引入的迭代器函数表迭代器这可以从这个快速示例中看出 (labels ((x())) (let ((y nil)) (declare (special xy)) (eval `(progn (princ ,y) (princ ,x) )))) 这里 y 没有问题,但 x 不能被声明为特殊的。如果 x 是 let 中的 lambda 则它可以工作
  • (1) 实际上,eval 在 null 词法环境中评估代码,这意味着您评估的代码无法访问调用 evals 的代码中的绑定(这很好); (2)你可以写一个你想做的宏,但是你需要在生成的代码中引入新鲜的符号以避免变量捕获,所以你需要调用gensym,我会编辑答案
猜你喜欢
  • 1970-01-01
  • 2012-07-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-09-06
  • 1970-01-01
相关资源
最近更新 更多