问题描述
前奏
在 Raku
中有一个名为 infinite list
AKA 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 中以最实用的方式实现这样的无限列表?
解决方法
对此的一个很好的教学方法是定义有时称为“流”的事物。据我所知,最好的介绍是在 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,但是这里太长了。
,出于实际目的,使用现有库是明智的,但由于问题是关于如何实现惰性列表,我们将从头开始。
关闭
延迟迭代是生成一个对象,每次被要求这样做时,该对象可以生成一个延迟序列的新值。 一个简单的方法是返回一个闭包,即一个关闭变量的函数,它在通过副作用更新其状态的同时产生值。
如果您评估:
(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
分配给正文中的不同值。
更准确地说,SHIFTF
将 n
分配给 (1+ n)
,但返回 n
的先前值。
例如:
(let ((it (inf 5)))
(list (funcall it)
(funcall it)
(funcall it)
(funcall it)))
给出:
(5 6 7 8)
通用迭代器
标准 dolist
需要一个正确的列表作为输入,您无法放置另一种数据并期望它工作(或者可能以特定于实现的方式)。
我们需要一个类似的宏来迭代任意迭代器中的所有值。
我们还需要指定迭代何时停止。
这里有多种可能性,让我们定义一个基本的迭代协议如下:
- 我们可以在任何对象上调用
make-iterator
以及任意参数,以获得迭代器 - 我们可以在迭代器上调用
next
来获取下一个值。 - 更准确地说,如果有值,
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"))
使用通用函数允许用户定义自定义迭代器,只要它们遵守上述指定的行为。
我们没有使用仅适用于 Eager 序列的 dolist
,而是定义了我们自己的宏:for
。
它隐藏了用户对 make-iterator
和 next
的调用。
换句话说,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
类的值 f
指定 function
,以便调用函数 f
:
(defmethod next ((f function))
"A closure is an interator"
(funcall f))
此外,我们还可以专门化 make-iterator
使闭包成为它们自己的迭代器(我认为没有其他好的默认行为可以为闭包提供):
(defmethod make-iterator ((function function) &key)
function)
向量迭代器
例如,我们可以为向量构建一个迭代器,如下所示。我们专门为 make-iterator
类的值(此处命名为 vec
)使用 vector
。
返回的迭代器是一个闭包,因此我们将能够对其调用 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
,
我会用一个库来展示它:
如何使用 GTWIWTG 生成器库创建和使用无限的整数列表
这个名为“以我想要的方式生成生成器”的库允许做三件事:
- 创建生成器(迭代器)
- 组合它们
- 食用它们(一次)。
它与近乎经典的系列并无不同。
使用 Error in UseMethod("st_intersection") : no applicable method for 'st_intersection' applied to an object of class "c('integer','numeric')"
安装库。我将使用它的包:(ql:quickload "gtwiwtg")
。
为无穷大的整数列表创建一个生成器,从 0 开始:
(in-package :gtwiwtg)
我们还可以指定其 GTWIWTG> (range)
#<RANGE-BACKED-GENERATOR! {10042B4D83}>
、:from
、:to
和 :by
参数。
将此生成器与其他生成器结合使用:此处不需要。
迭代并停止:
:inclusive
这个方案很实用:)