【问题标题】:Is there a straightforward lisp equivalent of Python's generators?是否有与 Python 生成器相当的直接 lisp?
【发布时间】:2016-01-02 13:09:36
【问题描述】:

在 Python 中你可以这样写:

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

什么是 lisp 等价物?

【问题讨论】:

    标签: lisp common-lisp generator


    【解决方案1】:

    现有包

    使用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 相同)。

    • Iterators in generic-cl

    关闭

    在实践中,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 场景(即突变,这恰好是大多数(所有?)实现所做的)并手动重新绑定迭代变量,如果它们要在以后被捕获和重用。

    【讨论】:

    猜你喜欢
    • 2011-05-25
    • 1970-01-01
    • 2012-04-16
    • 2010-10-22
    • 2011-01-09
    • 2011-04-20
    • 2011-04-21
    相关资源
    最近更新 更多