懒惰序列的思考

Aru*_*n R 20 clojure

以Clojure Wiki中的Fibonacci系列为例,Clojure代码为:

(def fib-seq
 (lazy-cat [0 1] (map + (rest fib-seq) fib-seq)))
Run Code Online (Sandbox Code Playgroud)

如果你从[0 1]开始考虑这个问题,它是如何工作的?如果对思考过程有任何建议,那就很好了.

seh*_*seh 37

如您所述,[0 1]建立基本情况:序列中的前两个值为零,然后是一个.之后,每个值都是前一个值和该值之前的值的总和.因此,我们甚至无法计算序列中的第三个值,而不会在它之前至少有两个值.这就是为什么我们需要两个值来开始.

现在看一下map表格.它表示从两个不同的序列中获取头项,将它们与+函数组合(添加多个值以产生一个和),并将结果作为序列中的下一个值公开.该map形式将两个序列(可能是相等长度)压缩成一个相同长度的序列.

馈送到的两个序列map是相同基本序列的不同视图,移位一个元素.第一个序列是"除基本序列的第一个值之外的所有值".第二个序列是基本序列本身,当然,它包括第一个值.但基本序列应该是什么?

上面的定义说每个新元素是前一个元素(Z - 1)和前一个元素(Z - 2)的前一个元素的总和.这意味着扩展值序列需要以相同的顺序访问先前计算的值.我们肯定需要一个双元素移位寄存器,但我们也可以请求访问我们以前的结果.这就是fib-seq这里所谓的序列的递归引用.符号fib-seq指的是一个序列,它是一个零,一个,然后是它自己的Z - 2Z - 1值之和的串联.

采用所调用的序列fib-seq,绘制第一个项会产生[0 1]向量的第一个元素- 零.绘制第二个项目会产生向量的第二个元素 - 一个.在绘制第三个项目时,我们咨询map生成序列并将其用作剩余值.map这里生成的序列以"其余部分"的第一项和第一项的总和开始[0 1],第一项[0 1]是零,第一项是零.这笔钱是一个.

绘制第四个项目map再次进行咨询,现在必须计算基本序列的"剩余部分"的第二项的总和,该基础序列是由基数序列生成的map,以及基本序列的第二项,即向量中的一项.[0 1].这笔钱是两个.

绘制第五项参考map,将基本序列的"剩余部分"的第三项 - 再次,由零和一的总和得到的那一项 - 以及我们刚刚发现的两个基本序列的第三项相加.

您可以看到它是如何构建的,以匹配系列的预期定义.更难看的是绘制每个项目是否重新计算所有前面的值两次 - 对于每个检查的序列一次map.事实证明这里没有这样的重复.

为了确认这一点,增加这样的定义fib-seq来仪器使用功能+:

(def fib-seq
  (lazy-cat [0 1] 
            (map 
              (fn [a b]
                (println (format "Adding %d and %d." a b))
                (+ a b))
            (rest fib-seq) fib-seq)))
Run Code Online (Sandbox Code Playgroud)

现在要求前十项:

> (doall (take 10 fib-seq))
Adding 1 and 0.
Adding 1 and 1.
Adding 2 and 1.
Adding 3 and 2.
Adding 5 and 3.
Adding 8 and 5.
Adding 13 and 8.
Adding 21 and 13.
(0 1 1 2 3 5 8 13 21 34)
Run Code Online (Sandbox Code Playgroud)

请注意,有八个调用+来生成前十个值.


在编写前面的讨论之后,我花了一些时间研究Clojure中懒惰序列的实现 - 特别是LazySeq.java文件- 并认为这是分享一些观察结果的好地方.

首先,请注意Clojure中的许多延迟序列处理函数最终会使用lazy-seq其他一些集合.lazy-seq创建Java类型的实例LazySeq,它为小型状态机建模.它有几个允许它以不同状态启动的构造函数,但最有趣的情况是仅以对nullary函数的引用开始的情况.以这种方式构造,LazySeq既没有评估函数也没有找到委托序列(ISeq在Java中输入).第一次询问LazySeq第一个元素 - via first- 或任何后继者 - 通过nextrest- 它评估函数,向下挖掘生成的对象以剥离其他LazySeq实例的任何包装层,最后通过java提供最内层对象function RT#seq(),导致一个ISeq实例.

在这一点上,LazySeq有一个ISeq到代表的委托电话first,nextrest.通常,"head" ISeq将是类型Cons,它在其"第一"(或"car")槽中存储恒定值,而ISeq在其"rest"(或"cdr")槽中存储另一个值.即ISeq在它的"休息"槽可以依次是LazySeq,在这种情况下访问它会再次需要该相同的功能的评价,剥离掉任何懒惰包装上的返回值,并通过该值通过RT#seq(),以产生另一个ISeq向其委托.

LazySeq情况下保持连接在一起,但是具有强制一个(通过first,nextrest)使其通过一些非延迟直委派ISeq其后.通常,强制计算一个函数,该函数产生一个Cons绑定到第一个值并且它的尾部绑定到另一个值LazySeq; 它是一个生成器函数链,每个函数生成一个值(Cons"s"第一个"槽"),链接到另一个机会以产生更多值(LazySeqCons's"rest"槽中).

在上面的Fibonacci序列示例中,map将这些内容绑定到每个嵌套引用,fib-seq并通过重复调用将它们分开rest.每个这样的调用最多只会将LazySeq一个未评估的函数转换为LazySeq指向类似的函数Cons.转换后,任何后续访问都将快速解析为Conses - 存储实际值的位置.当map压缩的一个分支将fib-seq一个元素引导到另一个元素后面时,这些值已经被解析并且可用于恒定时间访问,而不需要进一步评估所需的生成器功能.

以下是一些图表,可帮助您查看代码的这种解释:

        +---------+
        | LazySeq |
fib-seq |  fn -------> (fn ...)
        |  sv     |
        |  s      |
        +---------+


        +---------+
        | LazySeq |
fib-seq |  fn -------> (fn ...) -+
        |  sv <------------------+
        |  s      |
        +---------+


        +---------+
        | LazySeq |
fib-seq |  fn     |
        |  sv -------> RT#seq() -+
        |  s  <------------------+
        +---------+


        +---------+   +------+
        | LazySeq |   | ISeq |
fib-seq |  fn     |   |      |
        |  sv     |   |      |
        |  s  ------->|      |
        +---------+   +------+


        +---------+   +--------+        +------+
        | LazySeq |   | Cons   |        | ISeq |
fib-seq |  fn     |   |  first ---> 1   |      |
        |  sv     |   |  more  -------->|      |
        |  s  ------->|        |        |      |
        +---------+   +--------+        +------+


        +---------+   +--------+        +---------+
        | LazySeq |   | Cons   |        | LazySeq |
fib-seq |  fn     |   |  first ---> 1   |  fn -------> (fn ...)
        |  sv     |   |  more  -------->|  sv     |
        |  s  ------->|        |        |  s      |
        +---------+   +--------+        +---------+
Run Code Online (Sandbox Code Playgroud)

随着map进展,它从跳跃LazySeqLazySeq(并因此Cons到达Cons),并且最右边仅在第一次调用firstnext,或rest在给定时扩展LazySeq.


Jör*_*tag 12

我的Clojure有点生疏,但这似乎是着名的Haskell单行的字面翻译:

fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
Run Code Online (Sandbox Code Playgroud)

[我将使用伪Haskell,因为它更简洁一点.]

你需要做的第一件事就是让懒惰沉入其中.当你看到这样的定义时:

zeroes = 0 : zeroes
Run Code Online (Sandbox Code Playgroud)

作为一个严格的程序员,你直接的直觉反应是"ZOMG无限循环!必须修复bug!" 但它不是一个无限循环.这是一个懒惰的无限循环.如果你做一些愚蠢的事情

print zeroes
Run Code Online (Sandbox Code Playgroud)

然后,是的,有是一个无限循环.但只要你只使用有限数量的元素,你就永远不会注意到递归实际上并没有终止.这真的很难得到.我还是没有.

懒惰就像货币制度:它基于这样的假设:绝大多数人从不使用他们的绝大部分资金.所以,当把1000美元存入银行时,他们就不会把它保存在保险柜里.他们借给别人.实际上,他们利用这笔钱,这意味着他们向其他人借了5000美元.他们只在任何时候需要足够的钱,让他们能够迅速重新洗牌,以便它的存在,当你看着它,给你的外表,他们实际上让你的钱.

只要他们能够设法在你走到自动取款机时总是给钱,实际上你的绝大部分资金都不存在并不重要:他们只需要少量的资金来支付你的资金.你退学的时间.

懒惰的效果是一样的:每当你看到它,价值就在那里.如果你看一下第一个,第十个,第五个,第四个元素zeroes,它就会存在.但是,这只是在那里,如果你看它,不是之前.

这就是为什么这种无关紧要的递归定义的原因zeroes:只要你不试图查看无限列表的最后一个元素(或每个元素),你就是安全的.

下一步是zipWith.(Clojure的map仅仅是一个在其他编程语言是什么,通常有三种不同的功能概括:map,zipzipWith在这个例子中,它被用作zipWith.)

zip功能族以这种方式命名的原因是因为它实际上像拉链一样工作,这也是如何最好地可视化它.假设我们有一些体育赛事,每个参赛者都有两次尝试,两次尝试的分数相加,以得出最终结果.如果我们有两个序列,run1并且run2每次运行得分,我们可以像这样计算最终结果:

res = zipWith (+) run1 run2
Run Code Online (Sandbox Code Playgroud)

假设我们的两个列表是(3 1 6 8 6)(4 6 7 1 3),我们并排排列这两个列表,比如拉链的两半,然后我们使用给定的函数(+在本例中)将它们压缩在一起以产生一个新的序列:

3   1   6   8   6
+   +   +   +   +
4   6   7   1   3
=   =   =   =   =
7   7  13   9   9
Run Code Online (Sandbox Code Playgroud)

参赛者#3获胜.

那么,我们的fib样子是什么样的?

好吧,它从a开始0,然后我们追加a 1,然后我们追加无限列表的总和,并将无限列表移动一个元素.最简单的方法就是:

或者,如果你以不同的方式绘制它并将结果列表和第二个输入列表(实际上它们是相同的)合并为一个:

0   1   1   2   3   ^
+   +   +   +   +   |
1 = 1 = 2 = 3 = 5---+
Run Code Online (Sandbox Code Playgroud)

无论如何,这就是想象它的方式.