序列

序列可以看成是集合的一个逻辑视图。许多事物可以看成是序列。包括Java的集合,Clojure提供的集合,字符串,流,目录结构以及XML树。

很多Clojure的函数返回一个lazy序列(LazySeq), 这种序列里面的元素不是实际的数据, 而是一些方法, 它们直到用户真正需要数据的时候才会被调用。LazySeq的一个好处是在你创建这个序列的时候你不用太担心这个序列到底会有多少元素。下面是会返回lazySeq的一些函数: cache-seq , concat , cycle , distinct , drop , drop-last , drop-while , filter , for , interleave , interpose , iterate , lazy-cat , lazy-seq , line-seq , map , partition , range , re-seq , remove , repeat , replicate , take , take-nth , take-while and tree-seq

LazySeq是刚接触Clojure的人比较容易弄不清楚的一个东西。比如你们觉得下面这个代码的输出是什么?

(map #(println %) [1 2 3])

当在一个REPL里面运行的时候,它会输出 1, 2 和 3 在单独的行上面, 以及三个nil(三个println的返回结果)。REPL总是立即解析/调用我们所输入的所有的表达式。但是当作为一个脚本来运行的时候,这句代码不会输出任何东西。因为 map 函数返回的是一个LazySeq。

有很多方法可以强制LazySeq对它里面的方法进行调用。比如从序列里面获取一个元素的方法 first , second , nth 以及 last 都能达到这个效果。序列里面的方法是按顺序调用的, 所以你如果要获取最后一个元素, 那么整个LazySeq里面的方法都会被调用。

如果LazySeq的头被存在一个binding里面,那么一旦一个元素的方法被调用了, 那么这个元素的值会被缓存起来, 下次我们再来获取这个元素的时候就不用再调用函数了。

dorundoall 函数迫使一个LazySeq里面的函数被调用。 doseq 宏, 我们在 "迭代" 那一节提到过的, 会迫使一个或者多个LazySeq里面的函数调用。 for 宏, 也在是"迭代”那一节提到的,不会强制调用LazySeq里面的方法, 相反, 他会返回另外一个LazySeq。

为了只是简单的想要迫使LazySeq里面的方法被调用,那么 doseq 或者 dorun 就够了。调用的结果不会被保留的, 所以占用的内存也就比较少。这两个方法的返回值都是 nil . 如果你想调用的结果被缓存, 那么你应该使用 doall .

下面的表格列出来了强制LazySeq里面的方法被调用的几个办法。

结果要缓存 只要求方法被执行,不需要缓存
操作单个序列 doall dorun
利用list comprehension语法来操作多个序列 N/A doseq

一般来说我们比较推荐使用 doseq 而不是 dorun 函数, 因为这样代码更加易懂。 同时代码效率也更高, 因为dorun内部使用map又创建了另外一个序列。比如下面的两会的结果是一样的。

(dorun (map #(println %) [1 2 3]))
(doseq [i [1 2 3]] (println i))

如果一个方法会返回一个LazySeq并且在它的方法被调用的时候还会有副作用,那么大多数情况下我们应该使用 doall 来调用并且返回它的结果。这使得副作用的出现时间更容易确定。否则的话别的调用者可能会调用这个LazySeq多次,那么副作用也就会出现多次 -- 从而可能出现错误的结果。

下面的几个表达式都会在不同的行输出1, 2, 3, 但是它们的返回值是不一样的。 do special form 是用来实现一个匿名函数,这个函数先打印这个值, 然后再把这个值返回。

(doseq [item [1 2 3]] (println item)) ; -> nil
(dorun (map #(println %) [1 2 3])) ; -> nil
(doall (map #(do (println %) %) [1 2 3])) ; -> (1 2 3)

LazySeq使得创建无限序列成为可能。因为只有需要使用的数据才会在用到的时候被调用创建。比如

(defn f
  "square the argument and divide by 2"
  [x]
  (println "calculating f of" x)
  (/ (* x x) 2.0))

; Create an infinite sequence of results from the function f
; for the values 0 through infinity.
; Note that the head of this sequence is being held in the binding "f-seq".
; This will cause the values of all evaluated items to be cached.
(def f-seq (map f (iterate inc 0)))

; Force evaluation of the first item in the infinite sequence, (f 0).
(println "first is" (first f-seq)) ; -> 0.0

; Force evaluation of the first three items in the infinite sequence.
; Since the (f 0) has already been evaluated,
; only (f 1) and (f 2) will be evaluated.
(doall (take 3 f-seq))

(println (nth f-seq 2)) ; uses cached result -> 2.0

下面的代码和上面的代码不一样的地方是, 在下面的代码里面LazySeq的头没有被保持在一个binding里面, 所以被调用过的方法的返回值不会被缓存。所以它所需要的内存比较少, 但是如果同一个元素被请求多次, 那么它的效率会低一点。

(defn f-seq [] (map f (iterate inc 0)))
(println (first (f-seq))) ; evaluates (f 0), but doesn't cache result
(println (nth (f-seq) 2)) ; evaluates (f 0), (f 1) and (f 2)

另外一种避免保持LazySeq的头的办法是把这个LazySeq直接传给函数:

(defn consumer [seq]
  ; Since seq is a local binding, the evaluated items in it
  ; are cached while in this function and then garbage collected.
  (println (first seq)) ; evaluates (f 0)
  (println (nth seq 2))) ; evaluates (f 1) and (f 2)

(consumer (map f (iterate inc 0)))