是否有一个简单的lisp相当于Python的生成器?

时间:2015-10-05 19:07:42

标签: lisp common-lisp generator

在Python中你可以这样写:

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

这相当于什么是lisp?

1 个答案:

答案 0 :(得分:23)

现有包

使用GENERATORS下载,安装和加载Quicklisp系统。然后,使用包: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))

这是首选的方法,特别是因为不需要像生成器一样依赖分隔的延续。你的例子涉及一个状态,骰子示例不需要(有一个影响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的匹配)的数据源也定期提供一个抽象其控制流的宏。您可以按如下方式使用它:

(block 'outer
  (let ((counter 0) stack)
    (do-repeatedly-throw-dice (value 6)

      ;; For each iteration of the infinite loop,
      ;; VALUE is bound to a new random value.

      (if (<= (incf counter) 20)
        (push value stack)
        (return-from 'outer (nreverse stack))))))

与上述不同的是该块明确命名。这是因为DO-X通常可以在其身体周围定义NIL块,这意味着任何封闭的NIL块都被遮蔽。 隐式NIL块允许您轻松退出迭代:

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

宏的一个可能的实现是将主体包装成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))))

上述形式的宏观扩展的一步:

(BLOCK 'OUTER
  (LET ((COUNTER 0) STACK)
    (PROG ((#:G16053 6) VALUE)
     #:NEXT (SETF VALUE (1+ (RANDOM #:G16053)))
            (PROGN (IF (<= (INCF COUNTER) 20)
                      (PUSH VALUE STACK)
                      (RETURN-FROM 'OUTER (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中的大多数(如果不是全部)迭代器都倾向于这样工作,对于有经验的用户来说它不应该是一个惊喜(实际上相反的情况会令人惊讶)。如果通过重复调用闭包来实现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))))

如果您愿意,您可以随时从宏中引入内部let,但很少这样做。