序列
序列非常類似於列表:它是一個不可變物件,可以在恆定時間內為你提供 first
元素或其元素的 rest
。你還可以通過現有序列來切換新序列,並在開頭貼上一個專案。
你可以使用 seq?
謂詞測試某些內容是否為序列 :
(seq? nil)
;;=> false
(seq? 42)
;;=> false
(seq? :foo)
;;=> false
如你所知,列表是序列:
(seq? ())
;;=> true
(seq? '(:foo :bar))
;;=> true
通過在非空集合上呼叫 seq
或 rseq
或 keys
或 vals
獲得的任何內容也是一個序列:
(seq? (seq ()))
;;=> false
(seq? (seq '(:foo :bar)))
;;=> true
(seq? (seq []))
;;=> false
(seq? (seq [:foo :bar]))
;;=> true
(seq? (rseq []))
;;=> false
(seq? (rseq [:foo :bar]))
;;=> true
(seq? (seq {}))
;;=> false
(seq? (seq {:foo :bar :baz :qux}))
;;=> true
(seq? (keys {}))
;;=> false
(seq? (keys {:foo :bar :baz :qux}))
;;=> true
(seq? (vals {}))
;;=> false
(seq? (vals {:foo :bar :baz :qux}))
;;=> true
(seq? (seq #{}))
;;=> false
(seq? (seq #{:foo :bar}))
;;=> true
請記住,所有列表都是序列,但並非所有序列都是列表。雖然列表在常量時間內支援 peek
和 pop
和 count
,但通常,序列不需要支援任何這些函式。如果你試圖在一個不支援 Clojure 堆疊介面的序列上呼叫 peek
或 pop
,你會得到一個 ClassCastException
:
(peek (seq [:foo :bar]))
;; java.lang.ClassCastException: clojure.lang.PersistentVector$ChunkedSeq cannot be cast to clojure.lang.IPersistentStack
(pop (seq [:foo :bar]))
;; java.lang.ClassCastException: clojure.lang.PersistentVector$ChunkedSeq cannot be cast to clojure.lang.IPersistentStack
(peek (seq #{:foo :bar}))
;; java.lang.ClassCastException: clojure.lang.APersistentMap$KeySeq cannot be cast to clojure.lang.IPersistentStack
(pop (seq #{:foo :bar}))
;; java.lang.ClassCastException: clojure.lang.APersistentMap$KeySeq cannot be cast to clojure.lang.IPersistentStack
(peek (seq {:foo :bar :baz :qux}))
;; java.lang.ClassCastException: clojure.lang.PersistentArrayMap$Seq cannot be cast to clojure.lang.IPersistentStack
(pop (seq {:foo :bar :baz :qux}))
;; java.lang.ClassCastException: clojure.lang.PersistentArrayMap$Seq cannot be cast to clojure.lang.IPersistentStack
如果你在一個沒有在恆定時間內實現 count
的序列上呼叫 count
,你就不會得到錯誤; 相反,Clojure 將遍歷整個序列,直到它到達結尾,然後返回它遍歷的元素數。這意味著,對於一般序列,count
是線性的,而不是恆定的時間。你可以使用 counted?
謂詞測試某些內容是否支援常量時間 count
:
(counted? '(:foo :bar))
;;=> true
(counted? (seq '(:foo :bar)))
;;=> true
(counted? [:foo :bar])
;;=> true
(counted? (seq [:foo :bar]))
;;=> true
(counted? {:foo :bar :baz :qux})
;;=> true
(counted? (seq {:foo :bar :baz :qux}))
;;=> true
(counted? #{:foo :bar})
;;=> true
(counted? (seq #{:foo :bar}))
;;=> false
如上所述,你可以使用 first
來獲取序列的第一個元素。請注意,first
將在他們的引數上呼叫 seq
,因此它可用於任何 seqable
,而不僅僅是實際序列:
(first nil)
;;=> nil
(first '(:foo))
;;=> :foo
(first '(:foo :bar))
;;=> :foo
(first [:foo])
;;=> :foo
(first [:foo :bar])
;;=> :foo
(first {:foo :bar})
;;=> [:foo :bar]
(first #{:foo})
;;=> :foo
同樣如上所述,你可以使用 rest
來獲取包含除現有序列的第一個元素之外的所有元素的序列。像 first
一樣,它在論證中稱之為 seq
。但是,它並沒有呼叫 seq
它的結果! 這意味著,如果你在一個包含少於兩個專案的序列上呼叫 rest
,你將得到 ()
而不是 nil
:
(rest nil)
;;=> ()
(rest '(:foo))
;;=> ()
(rest '(:foo :bar))
;;=> (:bar)
(rest [:foo])
;;=> ()
(rest [:foo :bar])
;;=> (:bar)
(rest {:foo :bar})
;;=> ()
(rest #{:foo})
;;=> ()
如果你想在序列中沒有任何元素時返回 nil
,你可以使用 next
而不是 rest
:
(next nil)
;;=> nil
(next '(:foo))
;;=> nil
(next [:foo])
;;=> nil
你可以使用 cons
函式建立一個新的序列,它將返回 first
的第一個引數和 rest
的第二個引數:
(cons :foo nil)
;;=> (:foo)
(cons :foo (cons :bar nil))
;;=> (:foo :bar)
Clojure 提供了一個大型序列庫,它具有許多處理序列的功能。這個庫的重要之處在於它適用於任何 seqable
,而不僅僅是列表。這就是為什麼序列的概念是如此有用的原因; 這意味著像 reduce
這樣的單一功能可以完美地適用於任何集合:
(reduce + '(1 2 3))
;;=> 6
(reduce + [1 2 3])
;;=> 6
(reduce + #{1 2 3})
;;=> 6
序列有用的另一個原因是,由於它們不要求 first
和 rest
的任何特定實現,它們允許惰性序列,其元素僅在必要時實現。
給定一個可以建立序列的表示式,你可以將該表示式包裝在 lazy-seq
巨集中以獲取一個類似於序列的物件,但只有當 seq
函式要求它執行此操作時才會實際評估該表示式,此時它將快取表示式的結果並將 first
和 rest
呼叫轉發到快取的結果。
對於有限序列,惰性序列通常與等效的急切序列相同:
(seq [:foo :bar])
;;=> (:foo :bar)
(lazy-seq [:foo :bar])
;;=> (:foo :bar)
但是,對於無限序列,差異變得明顯:
(defn eager-fibonacci [a b]
(cons a (eager-fibonacci b (+' a b))))
(defn lazy-fibonacci [a b]
(lazy-seq (cons a (lazy-fibonacci b (+' a b)))))
(take 10 (eager-fibonacci 0 1))
;; java.lang.StackOverflowError:
(take 10 (lazy-fibonacci 0 1))
;;=> (0 1 1 2 3 5 8 13 21 34)