【发布时间】:2016-01-02 13:09:36
【问题描述】:
在 Python 中你可以这样写:
def firstn(n):
num = 0
while num < n:
yield num
num += 1
什么是 lisp 等价物?
【问题讨论】:
标签: lisp common-lisp generator
在 Python 中你可以这样写:
def firstn(n):
num = 0
while num < n:
yield num
num += 1
什么是 lisp 等价物?
【问题讨论】:
标签: lisp common-lisp generator
使用Quicklisp 下载、安装和加载GENERATORS 系统。然后,使用包: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)
但是请注意图书馆的作者所说的:
这个图书馆更像是一个有趣的玩具,虽然据我所知 确实有效。我认为我从未在应用程序代码中使用过它, 虽然我认为小心,它可能是。
ITERATE 包提供了一种定义 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))
....返回:
(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))
...返回:
(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 永远不会发生变异,因此闭包将不断返回相同的值。
如果您愿意,您可以随时从宏中引入内部 let,但很少这样做。
1:请注意,DOTIMES 的规范并不要求绑定在每次迭代时都是新鲜的,或者只在每个步骤中改变相同的绑定:“它是依赖于实现的dotimes 是否在每次迭代时都为 var 建立新绑定,或者是否在开始时为 var 建立一次绑定,然后在任何后续迭代中分配它。” 为了便于编写,有必要假设最坏的情况-case 场景(即突变,这恰好是大多数(所有?)实现所做的)并手动重新绑定迭代变量,如果它们要在以后被捕获和重用。
【讨论】: