Clojure China

请问下这个lazy-seq怎么用

#1

如标题

#2

(map inc [1 2 3]) 这个结果就是一个lazy-seq了,那么你还是可以继续做 reduce 或者 filter ,在没有特殊需要的情况下,当seq使用即可。

#3
(defn fib 
         ([] (fib 1 1))
         ([a b]
           (lazy-seq (cons a 
                                     (fib b (+ a b))))))

那这个呢

(take 5 (fib)) ;;(1,1,2,3,5)
#4

这个fib生成的是一个lazy-seq,他的长度是无限的,某些角度来看,内存都已经爆炸了。
而你没有爆炸,所以lazy, 只有在你调用的时候才进行求值.

#5

但是我不知道这个前面的列表元素怎么来的(1,),(1 ,1),(1,1,2)
你能从原先的代码里解释出来吗

#6

其实 @miaotou 的回答已经够了,只是 你给的这个例子其实是被三个概念 (cons, lazy-seq, 递归) 混在一起搞复杂化了,所以不好理解。 cons 和 lazy-seq 其实都很直观,但是这里 fib 是一个递归函数,把整个代码变复杂了 N 倍。

  • cons: 如果你学过链表的话,(cons x y) 从概念上相当于制造一个链表,表头是 x, next 指针就是指向 y
  • lazy-seq: (lazy-seq body) 会返回一个 lazy seq, 只有在遍历它时(比如对它调用 count/reduce 等函数时) 才会执行 body 来得到它包含的元素。所以 lazy-seq 的实现是一个 macro, 因为只有 macro 才能做到被调用时不先把参数求值。

你这个例子中 lazy-seq 的 body 是 (cons a (fib b (+ a b)), 所以:

  • cons 的链表头是一个明确的数字
  • 而 cons 的 next 指针部分又是一个对 fib 的调用, 所以它又是一个 lazy-seq+cons, 表头是 (a + b) …
  • 这样,所以整个 fib 函数返回的 lazy-seq 展开后的规律就是 (1,1, 2=1+1, 3=1+2)

你可以在函数体合适的地方加入 println 语句打印当前 a 和 b 的值来理解。

(defn fib 
  ([] (fib 1 1))
  ([a b]
   (println (str "a = " a ", b = " b))
   (lazy-seq (cons a 
                   (fib b (+ a b))))))

(def foo (doall (take 5 ( fib))))

; a = 1, b = 1
; a = 1, b = 2
; a = 2, b = 3
; a = 3, b = 5
; a = 5, b = 8
; a = 8, b = 13

要想更多的了解 lazy 的话,其它语言中也有 lazy 的概念:

  • python 中的 generator 几乎和 lazy seq 是一样的。
  • scala 中有 lazy val 的概念, “lazy val x = y * 1000” 这样的写法,是第一次使用 x 时才执行 y * 1000 这个计算,并且把结果缓存起来,以后每次用到 x 就不用再计算了。本质上只是一个语法糖。

clojure 中的 lazy seq 是最强大的,也是最不好理解和最容易被坑的。我觉得 lazy seq 的好处主要有两个,一是节省开销用到时再计算,用不到时就不计算。二是可以表达“无限序列”这样的数学上的概念直观地在代码中表达出来,好看又实用,典型的例子就是标准库中的 iterate, repeat, cycle 等等。