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.
另见
ITERATE
package provides a way to define generators 在其迭代设施中使用。
SERIES
包提供类似流的数据结构和对它们的操作。
Snakes 库(据我所知与 GENERATORS
的方法相同)。
-
闭包
实际上,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 建立一次绑定,然后在任何后续迭代中分配它,这取决于实现。" 为了可移植地编写,有必要假设最坏的情况(即突变,这恰好是大多数(所有?)实现所做的)并手动重新绑定迭代变量,如果它们要在以后被捕获和重用。
在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.
另见
ITERATE
package provides a way to define generators 在其迭代设施中使用。SERIES
包提供类似流的数据结构和对它们的操作。Snakes 库(据我所知与
GENERATORS
的方法相同)。
闭包
实际上,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 建立一次绑定,然后在任何后续迭代中分配它,这取决于实现。" 为了可移植地编写,有必要假设最坏的情况(即突变,这恰好是大多数(所有?)实现所做的)并手动重新绑定迭代变量,如果它们要在以后被捕获和重用。