以下Common Lisp宏中使用gensym有什么问题?

11

学习使用GNU CLISP 2.43的通用Lisp语言,可能会犯一些新手错误。例如,打印在x和y之间的质数。

(defun is-prime (n)
  (if (< n 2) (return-from is-prime NIL))

  (do ((i 2 (1+ i)))
      ((= i n) T)
    (if (= (mod n i) 0) 
        (return NIL))))

(defun next-prime-after (n)
  (do ((i (1+ n) (1+ i)))
      ((is-prime i) i)))

(defmacro do-primes-v2 ((var start end) &body body)
  `(do ((,var (if (is-prime ,start)
                  ,start
                  (next-prime-after ,start))
              (next-prime-after ,var)))
       ((> ,var ,end))
     ,@body))

(defmacro do-primes-v3 ((var start end) &body body)
  (let ((loop-start (gensym))
        (loop-end (gensym))) 
    `(do ((,loop-start ,start)
          (,loop-end ,end)
          (,var (if (is-prime ,loop-start)
                    ,loop-start
                    (next-prime-after ,loop-start))
                (next-prime-after ,var)))
         ((> ,var ,loop-end))
       ,@body )))

do-primes-v2运行完美。

[13]> (do-primes-v2 (p 10 25) (format t "~d " p))
11 13 17 19 23

接下来我尝试使用gensym来避免宏展开中的命名冲突 - do-primes-v3。然而,我卡在了一个

*** - EVAL: variable #:G3498 has no value

尝试使用macro-expand查看是否能发现错误,但我找不到。

[16]> (macroexpand-1 `(do-primes-v3 (p 10 25) (format t "~d " p)))
(DO
 ((#:G3502 10) (#:G3503 25)
  (P (IF (IS-PRIME #:G3502) #:G3502 (NEXT-PRIME-AFTER #:G3502))
     (NEXT-PRIME-AFTER P)))
 ((> P #:G3503)) (FORMAT T "~d " P)) ;

Lisp 似乎搞乱了 Stack Overflow 的语法着色。 - Gishu
它会这样做...尝试在行末添加一个终止引号的注释,以最小化影响。 - dsm
我发现这段代码中使用空格让人感到困惑。我可以将其调整为更接近标准格式吗? - Svante
请直接进行...我被认为这是LISP的方式 :) - Gishu
哎呀?Markdown好像有些问题。 - Svante
显示剩余2条评论
5个回答

5

请使用DO*代替DO

DO会在绑定还不可见的范围内初始化绑定。 DO*会在绑定可见的范围内初始化绑定。

在这种情况下,var需要引用另一个绑定loop-start


对头了...之前不知道DO,但是有读过LET和LET,所以可以联系起来。谢谢! - Gishu
解决方案:问题出在第三个变量的初始化表单需要第一个变量,而在DO中这是不可能的(尽管步骤表单可以做到这一点,因为它们在每次迭代结束时被评估)。要访问在变量列表中早期定义的init-form中的变量,请使用DO*。 - Gishu

4
你实际上并不需要在这里使用gensym来避免变量捕获,因为你没有引入任何变量,这些变量会“局部于宏”。当你展开do-primes-v2宏时,你会发现没有引入任何在宏外不存在的变量。
然而,你确实需要它来避免多次计算。
如果你像这样调用宏:
(do-primes-v2 (p (* x 2) (* y 3)) (format "~a~%" p))
它会扩展为
(do ((p (if (is-prime (* x 2)) (* x 2) (next-prime-after (* x 2))) (next-prime-after p))) ((> p (* y 3))) (format "~a~%" p))
最好的情况是,这是低效的,因为那些乘法操作被多次执行。然而,如果你使用具有副作用的函数作为输入,比如setfincf,这可能是一个大问题。

我已经阅读了《实用通用Lisp》的三分之一,正在阅读有关宏设计常见陷阱的内容,其中包括指南#1:确保您不会对传递给宏的表达式进行超过n次(n是绝对必要的次数)的求值。 - Gishu
1
是的,但你刚才写道你“尝试使用gensym来避免宏展开中的命名冲突”,我想澄清这里需要解决的问题是不同的。 - Svante

3

要么将循环开始和循环结束的绑定移到封闭的LET块中,要么使用DO*。原因是在DO中所有循环变量都是“并行”绑定的,所以对于第一个绑定,(扩展后的)循环开头变量还没有绑定。


不得不把最快手的帐号给kmkaplan - 你的回复也很有帮助 +1 - Gishu

1

我知道这并不完全回答了你的问题,但我认为这是相关的。根据我的经验,你试图编写的宏类型是非常常见的。我对你处理这个问题的方式有一个问题,它不能处理另一个常见用例:函数组合。

我没有时间强调您在使用宏时可能会遇到的一些困难,但是我将强调,如果您构建的素数迭代器面向函数组合,那么您的宏就变得非常简单,从而避免了你的问题。

注:我稍微修改了一些你的函数。

(defun is-prime (n)
  (cond
    ((< n 2)
     nil)
    ((= n 2)
     t)
    ((evenp n)
     nil)
    (t
     (do ((i 2 (1+ i)))
     ((= i n) t)
       (when (or (= (mod n i) 0))
         (return nil))))))

(defun next-prime (n)
  (do ((i n (1+ i)))
      ((is-prime i) i)))

(defun prime-iterator (start-at)
  (let ((current start-at))
    (lambda ()
      (let ((next-prime (next-prime current)))
         (setf current (1+ next-prime))
         next-prime))))

(defun map-primes/iterator (fn iterator end)
  (do ((i (funcall iterator) (funcall iterator)))
      ((>= i end) nil)
    (funcall fn i)))

(defun map-primes (fn start end)
  (let ((iterator (prime-iterator start)))
    (map-primes/iterator fn iterator end)))

(defmacro do-primes ((var start end) &body body)
  `(map-primes #'(lambda (,var)
                   ,@body)
               ,start ,end))

我也建议您查看Series。生成器模式在Lisp程序中也是非常常见的。您还可以查看Alexandria,特别是函数ALEXANDRIA:COMPOSE,以了解您可以使用函数组合实现哪些酷炫的功能。


学完LISP后,我会把这个加入到我的“待学习清单”中。 - Gishu

0

我建议完全避免使用DO/DO*和宏,而是选择Series(可以在series.sourceforge.net上找到其实现)。

如果这太复杂了,那么考虑使用递归或generator(用于按需生成)生成质数列表。


我正在学习宏的相关知识,所以这并没有完全解决我的问题。不过还是感谢你分享这个信息。 - Gishu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接