关于clojure:此代码如何运行惰性序列

How lazy sequence run in this code

代码在这里:

1
(def fib-seq (lazy-cat [0 1]  (map + (rest fib-seq) fib-seq )))

据我了解,fib-seq是一个惰性序列生成器,可生成一系列斐波那契数。
通过查看(take 5 fib-seq),我将得到如下斐波那契数:
(0 1 1 2 3)

但是我无法弄清楚在需要时如何生成惰性序列,因此我对其添加了一些副作用。

1
2
3
(def fib-seq (lazy-cat [0 1] (map +
    (do (println"R") (rest fib-seq))
    (do (println"B") fib-seq))))

通过添加println,我希望每当惰性序列在需要时尝试生成新条目时,它都会打印出RB,但是不幸的是,事实证明是这样的。

1
2
3
4
user=> (take 5 fib-seq) ; this is the first time I take 5 elements
(0 R
B
1 1 2 3)

上面的输出看起来已经很奇怪了,因为它没有逐元素打印R和B,但是让我们看一下下一步。

第一次获取元素后:

1
2
user=> (take 20 fib-seq)
(0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181)

我再也不会收到RB了,这使我感到困惑,因为它与我对惰性序列生成的理解相冲突。

有什么可以向我逐步解释吗?
顺便说一句,是否有可能像JavaC一样具有debug实用程序来调试step by step


好的,一步一步来:

  • 这是lazy-cat(链接)的源代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    (defmacro lazy-cat
     "Expands to code which yields a lazy sequence of the concatenation
      of the supplied colls.  Each coll expr is not evaluated until it is
      needed.

      (lazy-cat xs ys zs) === (concat (lazy-seq xs) (lazy-seq ys) (lazy-seq zs))"
      {:added"1.0"}
      [& colls]
      `(concat ~@(map #(list `lazy-seq %) colls)))

    所以你的代码:

    1
    2
    3
    (def fib-seq (lazy-cat [0 1] (map +
        (do (println"R") (rest fib-seq))
        (do (println"B") fib-seq))))

    扩展为:

    1
    2
    3
    4
    (def fib-seq (concat (lazy-seq [0 1])
                         (lazy-seq (map +
                                        (do (println"R") (rest fib-seq))
                                        (do (println"B") fib-seq)))))

    concat本身返回一个惰性序列,这意味着在您遍历fib-seq之前不会评估concat形式的主体。

  • 第一次遍历fib-seq时(当您使用第一个5元素时),首先评估concat表单的主体:

    1
    2
    3
    4
    (concat (lazy-seq [0 1])
            (lazy-seq (map +
                           (do (println"R") (rest fib-seq))
                           (do (println"B") fib-seq))))

    concat返回的惰性序列的前两个元素来自(lazy-seq [0 1]),而后者又来自[0 1];在此之后,[0 1]会用尽,(lazy-seq [0 1])也会用尽,并且concat序列的下一个元素取自(lazy-seq (map ...))子序列。

    在这里,do特殊形式都得到了评估,并且都看到了RBdo的语义是评估其中的所有形式,然后返回最后一个形式的结果。

    因此(do (println"R") (rest fib-seq)打印R,然后返回(rest fib-seq)的结果,而(do (println"B") fib-seq))打印B,然后返回fib-seq

    (map ...)返回一个惰性序列;当遍历到达fib-seq的第三个元素时,将评估map序列的第一个元素;它是fib-seq的第一个元素(即0)和(rest fib-seq)的第一个元素,即fib-seq的第二个元素(即1)之和。两者都已在此时进行评估,因此我们不会以无限递归结束。

    对于下一个元素,map的惰性阻止了无限递归的发生,并且产生了魔力。

  • fib-seq的第二遍历(即(take 20 fib-seq))上,已经对它的前几个元素进行了评估,因此do特殊形式没有重新评估,并且遍历继续且没有副作用。

  • 要在每次从(rest fib-seq)fib-seq中提取新元素时打印RB,您必须这样做:

    1
    2
    3
    4
    5
    (def fib-seq
      (lazy-cat [0 1]
                (map +
                     (map #(do (println"R") %) (rest fib-seq))
                     (map #(do (println"B") %) fib-seq)))))


    感谢@omiel提供了许多有用的信息,但是仍然没有碰到最敏感的地方,经过一会儿的思考,我弄清楚了在生成惰性序列时发生了什么。
    如果我确实错了,可以在clojure master中纠正我。

    我的意思是逐步一步一步地专注于延迟序列项的生成,我已经知道了clojure语言的一些逻辑。

    我们知道fib-seq被定义为lazy-seq,并且其前两个项目分别为0和1,其余项目仍未评估,这是clojure的最有趣的功能。
    虽然很容易理解访问前两项只是意味着要触摸这两件事,并且它们在内存中或已缓存,所以可以直接将它们返回并打印出来。

    由于fib-seq目前没有第三项,因此当线程需要访问第三项时需要生成它,这是我的假设开始的地方:

    由于(map + (rest fib-seq) fib-seq )本身就是lazy-seq,因此它当前不包含任何项目,并等待对其调用more命令。
    在这里,调用fib-seq的第三项意味着调用延迟序列(map...)的第一项,因此它需要生成并实际执行代码。
    通过简单地用list替换变量名,map的代码如下所示:

    1
    (map + (rest [0 1 ..]) [0 1 ..] ); the '..' means it is a lazy sequence

    然后执行rest之后,此代码将变为以下代码:

    1
    2
    3
    4
    5
    6
    (map + [1 ..]  [0 1 ..] )
            ^       ^
            | ----- |
                |
                +
                1

    map生成惰性序列时,它被指示生成它的第一项,因此,通过map这两个列表,我们得到了一个项1=(+ 1 0),这是这两个列表的第一项都加在一起的结果。

    然后map停止生成项目,因为它没有指令。现在,在生成新项1并将其与[0 1]连接之后,我们的fib-seq现在看起来像这样:

    1
    [0 1 1 ..]

    非常好。现在,让我们触摸(nth fib-seq 4)fib-seq的第四项。
    fib-seq发现它不包含索引为4的项目,但发现第三个被缓存了,因此它将从3rd一个项目生成4th项目。

    现在线程移到(map ...)函数,并指示map分发它的第二项。
    地图发现它没有2号物品,因此必须生成它。并将fib-seq替换为真正的延迟序列:

    1
    (map + (rest [0 1 1..]) [0 1 1..] )

    然后,当然rest得到其余的seq:

    1
    (map + [1 1..] [0 1 1..] )

    我认为,这里最棘手的事情发生了。
    map同时添加这些列表的第二个而不是第一个:

    1
    2
    3
    4
    5
    6
    (map + [1 1..] [0 1 1..] )
              ^       ^
              | ----- |
                  |
                  +
                  2

    因此,地图可以将2作为其第二项返回,以完成指令。

    lazy-seq在得到指示时遵循跟随项中的相同策略,并将每个生成的项缓存在内存中以加快访问速度。

    对于此Fibonacci number generator,它只需移动两个列表并一一添加即可,然后递归生成所需的斐波那契数,如下所示:

    1
    2
    0 1 1 2 3 5 ..  
    1 1 2 3 5 ..

    当然,哪一种是生成Fibo的非常灵巧的方法。

    摘要

    总而言之,从人类的角度来看,惰性序列会始终从其最后一个状态/位置生成项目,而不是从其初始状态开始生成项目。

    如果我错了,请指正我,我是clojure中的新手,我渴望很好地学习它。

    BTW

    我想为clojureleiningen集成编写netbeans language plugin,因为我认为lighttable没有用,有人有什么建议吗?