1

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

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

我得到“评估中止 #<UNDEFINED-FUNCTION NEXT-ENTRY {100229C693}>”我不明白为什么下一个条目似乎对我创建的宏不可见。

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

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

4

1 回答 1

3

在您的代码中,宏在多个地方以受变量捕获 (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绑定为零(您通常不希望这种行为)。

改写一个函数

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

如果我理解正确,您的函数会按条目迭代哈希表。我假设vars拥有一个(key value)符号对列表,例如((k1 v1) (k2 v2)). 然后,body对带中的所有键/值对起作用。

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

请注意,在您的代码中,您只有一个 loop,它使用哈希表迭代器构建一个带。但是,由于宏名为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不破坏展开代码的变量。

于 2021-08-10T08:57:15.417 回答