【问题标题】:Implementing infinite list of consecutive integers in Lisp for lazy evaluation在 Lisp 中实现无限连续整数列表以进行惰性求值
【发布时间】:2021-04-26 02:55:00
【问题描述】:

前奏

Raku 中有一个称为infinite list 又名lazy list 的概念,其定义和使用如下:

my @inf = (1,2,3 ... Inf);
for @inf { say $_;
           exit if $_ == 7 }
# => OUTPUT
1
2
3
4
5
6
7

我想在 Common Lisp 中实现这种东西,特别是一个无限的连续整数列表,例如:

(defun inf (n)
  ("the implementation"))

这样

(inf 5)
=> (5 6 7 8 9 10 .... infinity)
;; hypothetical output just for the demo purposes. It won't be used in reality

然后我会用它来做这样的惰性求值:

(defun try () ;; catch and dolist 
  (catch 'foo ;; are just for demo purposes
    (dolist (n (inf 1) 'done)
      (format t "~A~%" n)
      (when (= n 7)
    (throw 'foo x)))))

CL-USER> (try)
1
2
3
4
5
6
7
; Evaluation aborted.

如何在 CL 中以最实用的方式实现这样一个无限列表?

【问题讨论】:

  • 如果列表是无限的,为什么程序会输出'done'?
  • try 函数仅用于演示。它在无限列表上进行迭代,并在满足某个条件时停止。
  • 您的示例用例需要的不仅仅是实现惰性列表的能力;它还需要dolist 了解惰性列表。
  • 这里an answer by me 展示了几种方法,即使在Scheme中;转换为 Common Lisp 应该很容易。

标签: lisp common-lisp lazy-evaluation raku infinite-sequence


【解决方案1】:

出于实际目的,使用现有库是明智的,但由于问题是关于如何实现惰性列表,我们将从头开始。

关闭

惰性迭代是指生成一个对象,该对象在每次被请求时都可以生成惰性序列的新值。 一个简单的方法是返回一个闭包,即一个关闭变量的函数,它在通过副作用更新其状态的同时产生值。

如果你评估:

(let ((a 0))
  (lambda () (incf a)))

您获得了一个具有局部状态的函数对象,即这里名为a 的变量。 这是对该函数专有位置的词法绑定,如果您对同一表达式再次求值,您将获得一个不同的匿名函数,该函数具有自己的本地状态。

当你调用闭包时,a 中存储的值会递增并返回它的值。

让我们将这个闭包绑定到一个名为counter的变量,多次调用它并将连续的结果存储在一个列表中:

(let ((counter (let ((a 0))
                 (lambda () (incf a)))))
  (list (funcall counter)
        (funcall counter)
        (funcall counter)
        (funcall counter)))

结果列表是:

(1 2 3 4)

简单的迭代器

在您的情况下,您希望在编写时有一个从 5 开始计数的迭代器:

(inf 5)

这可以实现如下:

(defun inf (n)
  (lambda ()
    (shiftf n (1+ n))))

这里不需要添加let,参数到n的词法绑定是在调用函数时完成的。 随着时间的推移,我们将n 分配给正文中的不同值。 更准确地说,SHIFTFn 分配给(1+ n),但返回n 的先前值。 例如:

(let ((it (inf 5)))
  (list (funcall it)
        (funcall it)
        (funcall it)
        (funcall it)))

这给出了:

(5 6 7 8)

通用迭代器

标准dolist 需要一个正确的列表作为输入,您无法放置另一种数据并期望它工作(或者可能以特定于实现的方式)。 我们需要一个类似的宏来迭代任意迭代器中的所有值。 我们还需要指定迭代停止的时间。 这里有多种可能,让我们定义一个基本的迭代协议如下:

  1. 我们可以在任何对象上调用make-iterator,连同任意参数,以获得一个迭代器
  2. 我们可以在迭代器上调用next来获取下一个值。
  3. 更准确地说,如果有值,next 返回该值,并将 T 作为辅助值;否则,next 返回 NIL。

让我们定义两个通用函数:

(defgeneric make-iterator (object &key)
  (:documentation "create an iterator for OBJECT and arguments ARGS"))

(defgeneric next (iterator)
  (:documentation "returns the next value and T as a secondary value, or NIL"))

使用泛型函数允许用户定义自定义迭代器,只要它们尊重上述指定的行为。

我们没有使用仅适用于急切序列的dolist,而是定义了自己的宏:for。 它隐藏了用户对make-iteratornext 的调用。 换句话说,for 接受一个对象并对其进行迭代。 我们可以用(return v) 跳过迭代,因为for 是用loop 实现的。

(defmacro for ((value object &rest args) &body body)
  (let ((it (gensym)) (exists (gensym)))
    `(let ((,it  (make-iterator ,object ,@args)))
       (loop
         (multiple-value-bind (,value ,exists) (next ,it)
           (unless ,exists
             (return))
           ,@body)))))

我们假设任何函数对象都可以充当迭代器,因此我们将next 专门用于function 类的值f,以便调用函数f

(defmethod next ((f function))
  "A closure is an interator"
  (funcall f))

此外,我们还可以专门化 make-iterator 以使闭包成为自己的迭代器(我认为没有其他好的默认行为可以为闭包提供):

(defmethod make-iterator ((function function) &key)
  function)

向量迭代器

例如,我们可以为向量构建一个迭代器,如下所示。我们将make-iterator 专门用于类vector 的值(这里命名为vec)。 返回的迭代器是一个闭包,所以我们可以在它上面调用next。 该方法接受默认为零的:start 参数:

(defmethod make-iterator ((vec vector) &key (start 0))
  "Vector iterator"
  (let ((index start))
    (lambda ()
      (when (array-in-bounds-p vec index)
        (values (aref vec (shiftf index (1+ index))) t)))))

你现在可以写了:

(for (v "abcdefg" :start 2)
  (print v))

这会打印以下字符:

#\c 
#\d 
#\e 
#\f 
#\g

列表迭代器

同样,我们可以构建一个列表迭代器。 这里为了演示其他类型的迭代器,让我们有一个自定义的光标类型。

(defstruct list-cursor head)

光标是一个对象,它保持对正在访问的列表中当前 cons-cell 的引用,即 NIL。

(defmethod make-iterator ((list list) &key)
  "List iterator"
  (make-list-cursor :head list))

我们定义next如下,专门针对list-cursor

(defmethod next ((cursor list-cursor))
  (when (list-cursor-head cursor)
    (values (pop (list-cursor-head cursor)) t)))

范围

Common Lisp 还允许使用 EQL 专门器来专门化方法,这意味着我们提供给 for 的对象可能是特定的关键字,例如 :range

(defmethod make-iterator ((_ (eql :range)) &key (from 0) (to :infinity) (by 1))
  (check-type from number)
  (check-type to (or number (eql :infinity)))
  (check-type by number)
  (let ((counter from))
    (case to
      (:infinity
       (lambda () (values (incf counter by) t)))
      (t
       (lambda ()
         (when (< counter to)
           (values (incf counter by) T)))))))

make-iterator 的可能调用是:

(make-iterator :range :from 0 :to 10 :by 2)

这也返回一个闭包。 例如,在这里,您将迭代一个范围,如下所示:

(for (v :range :from 0 :to 10 :by 2)
  (print v))

以上展开为:

(let ((#:g1463 (make-iterator :range :from 0 :to 10 :by 2)))
  (loop
   (multiple-value-bind (v #:g1464)
       (next #:g1463)
     (unless #:g1464 (return))
     (print v))))

最后,如果我们对inf进行小修改(添加辅助值):

(defun inf (n)
  (lambda ()
    (values (shiftf n (1+ n)) T)))

我们可以这样写:

(for (v (inf 5))
  (print v)
  (when (= v 7)
    (return)))

哪些打印:

5 
6 
7

【讨论】:

    【解决方案2】:

    一个很好的教学方法是定义有时称为“流”的事物。我所知道的关于这样做的最好的介绍是Structure and Interpretation of Computer Programs。流在第 3.5 节中进行了介绍,但不要仅仅阅读它:认真阅读这本书:这是一本每个对编程感兴趣的人都应该阅读的书。

    SICP使用Scheme,这种东西在Scheme中比较自然。但它可以在 CL 中相当容易地完成。我在下面写的是相当“Schemy”的 CL:特别是我只是假设尾调用是优化的。这在 CL 中不是一个安全的假设,但如果您的语言能够胜任,那么看看如何将这些概念构建到一种还没有这些概念的语言中就足够了。

    首先,我们需要一个支持惰性评估的构造:我们需要能够“延迟”某些东西以创建一个“承诺”,该承诺仅在需要时才被评估。好吧,函数的作用是只在被要求时才评估它们的身体,所以我们将使用它们:

    (defmacro delay (form)
      (let ((stashn (make-symbol "STASH"))
            (forcedn (make-symbol "FORCED")))
        `(let ((,stashn nil)
               (,forcedn nil))
           (lambda ()
             (if ,forcedn
                 ,stashn
               (setf ,forcedn t
                     ,stashn ,form))))))
    
    (defun force (thing)
      (funcall thing))
    

    delay 有点繁琐,它想确保一个承诺只被强制执行一次,并且它还想确保被延迟的表单不会被它用来执行此操作的状态所感染。你可以追踪delay 的扩展来看看它是做什么的:

    (delay (print 1))
     -> (let ((#:stash nil) (#:forced nil))
          (lambda ()
            (if #:forced #:stash (setf #:forced t #:stash (print 1)))))
    

    这很好。

    所以现在,我们将发明流:流就像 conses(它们是 conses!)但它们的 cdr 是延迟的:

    (defmacro cons-stream (car cdr)
      `(cons ,car (delay ,cdr)))
    
    (defun stream-car (s)
      (car s))
    
    (defun stream-cdr (s)
      (force (cdr s)))
    

    好的,让我们编写一个函数来获取流的第 n 个元素:

    (defun stream-nth (n s)
      (cond ((null s)
             nil)
            ((= n 0) (stream-car s))
            (t
             (stream-nth (1- n) (stream-cdr s)))))
    

    我们可以测试一下:

    > (stream-nth 2
                  (cons-stream 0 (cons-stream 1 (cons-stream 2 nil))))
    2
    

    现在我们可以编写一个函数来枚举自然数中的一个区间,默认情况下它将是一个半无限区间:

    (defun stream-enumerate-interval (low &optional (high nil))
      (if (and high (> low high))
          nil
          (cons-stream
           low
           (stream-enumerate-interval (1+ low) high))))
    

    现在:

    > (stream-nth 1000 (stream-enumerate-interval 0))
    1000
    

    等等。

    好吧,我们想要某种可以让我们遍历流的宏:类似于dolist,但用于流。好吧,我们可以通过首先编写一个函数来为流中的每个元素调用一个函数来做到这一点(这 不是 我在生产 CL 代码中这样做的方式,但在这里很好):

    (defun call/stream-elements (f s)
      ;; Call f on the elements of s, returning NIL
      (if (null s)
          nil
        (progn
          (funcall f (stream-car s))
          (call/stream-elements f (stream-cdr s)))))
    

    现在

    (defmacro do-stream ((e s &optional (r 'nil)) &body forms)
      `(progn
         (call/stream-elements (lambda (,e)
                                 ,@forms)
                               ,s)
         ,r))
    

    例如现在

    (defun look-for (v s)
      ;; look for an element of S which is EQL to V
      (do-stream (e s (values nil nil))
        (when (eql e v)
          (return-from look-for (values e t)))))
    

    然后我们可以说

    > (look-for 100 (stream-enumerate-interval 0))
    100
    t
    

    嗯,要让流真正有用,您还需要更多机制:您需要能够组合它们、附加它们等等。 SICP有很多这样的功能,一般很容易转成CL,但是这里太长了。

    【讨论】:

    【解决方案3】:

    我会用一个库来展示它:

    如何使用GTWIWTG 生成器库创建和使用无限的整数列表

    这个名为“Generators The Way I Want Them Generated”的库允许做三件事:

    • 创建生成器(迭代器)
    • 将它们组合起来
    • 食用它们(一次)。

    它与近乎经典的系列并无不同。

    使用(ql:quickload "gtwiwtg") 安装库。我将在它的包中工作:(in-package :gtwiwtg)

    为无限的整数列表创建一个生成器,从 0 开始:

    GTWIWTG> (range)
    #<RANGE-BACKED-GENERATOR! {10042B4D83}>
    

    我们也可以指定它的:from:to:by:inclusive参数。

    将此生成器与其他生成器结合使用:此处不需要。

    遍历它并停止:

    GTWIWTG> (for x *
               (print x)
               (when (= x 7)
                 (return)))
    
    0 
    1 
    2 
    3 
    4 
    5 
    6 
    7 
    T
    

    这个方案很实用:)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-06-27
      • 2019-09-03
      • 2019-11-16
      • 1970-01-01
      • 1970-01-01
      • 2021-05-12
      • 2016-01-18
      • 2017-11-01
      相关资源
      最近更新 更多