Python 的生成器是否有直接的 lisp 等价物?

Is there a straightforward lisp equivalent of Python's generators?

在Python中你可以这样写:

def firstn(n):
     num = 0
     while num < n:
         yield num
         num += 1

这在 lisp 中的等价物是什么?

现有包

下载、安装并加载 GENERATORS system with Quicklisp。然后,使用 package :generators(或者最好先定义自己的包)。

(ql:quickload :generators)
(use-package :generators)

为随机值定义无限生成器:

(defun dice (n)
  (make-generator ()
    ;; repeatedly return a random value between 1 and N
    (loop (yield (1+ (random n))))))

使用生成器:

(loop
   with dice = (dice 6)
   repeat 20
   collect (next dice))

=> (1 2 6 1 1 4 4 2 4 3 6 2 1 5 6 5 1 5 1 2)

但是请注意库的作者所说的内容:

This library is more of an interesting toy, though as far as I know it does work. I dont think I have ever used this in application code, though I think that with care, it could be.

另见

闭包

实际上,CL 并不像 Python 那样依赖于生成器。相反,当人们需要惰性序列时,他们会使用闭包:

(defun dice (n)
  (lambda ()
    (1+ (random n))))

然后,next 的等效项只是对 dice 生成的 thunk 的调用:

(loop
   with dice = (dice 6)
   repeat 20
   collect (funcall dice))

这是首选方法,特别是因为不需要像生成器那样依赖分隔的延续。您的示例涉及一个状态,dice 示例不需要(有一个隐藏状态影响 random,但这是另一回事)。以下是您的计数器的典型实现方式:

(defun first-n (n)
  (let ((counter -1))
    (lambda ()
      (when (< counter n)
        (incf counter)))))

高阶函数

或者,您设计一个接受回调函数的生成器,生成器会为每个值调用该回调函数。可以使用任何 funcallable,这允许调用者保留对代码执行的控制:

(defun repeatedly-throw-dice (n callback)
  (loop (funcall callback (1+ (random n)))))

然后,您可以按如下方式使用它:

(prog ((counter 0) stack)
  (repeatedly-throw-dice 6 
    (lambda (value)
      (if (<= (incf counter) 20)
        (push value stack)
        (return (nreverse stack))))))

请参阅 PROG 的文档。

do-traversal成语

提供自定义生成值方式的数据源(如字符串中 regular expressions 的匹配)也定期提供抽象其控制流的宏,而不是构建函数。您将按如下方式使用它:

 (let ((counter 0)  stack)
   (do-repeatedly-throw-dice (value 6)
     (if (<= (incf counter) 20)
       (push value stack)
       (return (nreverse stack))))))

DO-X 宏应该在它们的主体周围定义一个 NIL 块,这就是上面的 return 有效的原因。

宏的一种可能实现方式是将主体包装在 lambda 形式中并使用上面定义的基于回调的版本:

(defmacro do-repeatedly-throw-dice ((var n) &body body)
  `(block nil (repeatedly-throw-dice ,n (lambda (,var) ,@body))))

直接扩展成循环也是可以的:

(defmacro do-repeatedly-throw-dice ((var n) &body body)
  (let ((max (gensym)) (label (make-symbol "NEXT")))
    `(prog ((,max ,n) ,var)
        ,label
        (setf ,var (1+ (random ,max)))
        (progn ,@body)
        (go ,label))))

上式宏展开一步:

(prog ((#:g1078 6) value)
 #:next
  (setf value (1+ (random #:g1078)))
  (progn
   (if (<= (incf counter) 20)
       (push value stack)
       (return (nreverse stack))))
  (go #:next))

绑定

从广义上讲,使用高阶函数或直接使用 do- 宏构建生成器会产生相同的结果。你可以用另一个来实现(就个人而言,我更喜欢先定义宏,然后使用宏定义函数,但相反的做法也很有趣,因为你可以重新定义函数而无需重新编译宏的所有用法)。

但是,仍然有区别:宏在迭代中重复使用同一个变量,而闭包每次都引入一个新的绑定。例如:

(let ((list))
  (dotimes (i 10) (push (lambda () i) list))
  (mapcar #'funcall list))

.. returns:

(10 10 10 10 10 10 10 10 10 10)

Common Lisp 中的大多数(如果不是全部)迭代器都倾向于这样工作1,这并不奇怪对于有经验的用户(事实上,相反的情况会令人惊讶)。如果dotimes是通过重复调用闭包实现的,结果会不一样:

(defmacro my-dotimes ((var count-form &optional result-form) &body body)
  `(block nil
     (alexandria:map-iota (lambda (,var) ,@body) ,count-form)
     ,result-form))

通过上面的定义,我们可以看出:

(let ((list))
  (my-dotimes (i 10) (push (lambda () i) list))
  (mapcar #'funcall list))

...returns:

(9 8 7 6 5 4 3 2 1 0)

为了获得与标准dotimes相同的结果,您只需要在构建闭包之前创建一个新的绑定:

(let ((list))
  (dotimes (i 10) 
    (let ((j i))
      (push (lambda () j) list))))

这里j是一个新的绑定,它的值是i在关闭创建时的当前值; j 永远不会发生变化,因此闭包将始终 return 相同的值。 如果你愿意,你总是可以从宏中引入内部 let,但很少这样做。


1:请注意 DOTIMES 的规范不要求绑定在每次迭代时都是新鲜的,或者只在每一步改变相同的绑定:"dotimes 是在每次迭代时建立一个新的 var 绑定,还是在开始时为 var 建立一次绑定,然后在任何后续迭代中分配它,这取决于实现。" 为了可移植地编写,有必要假设最坏的情况(即突变,这恰好是大多数(所有?)实现所做的)并手动重新绑定迭代变量,如果它们要在以后被捕获和重用。