序列
序列非常类似于列表:它是一个不可变对象,可以在恒定时间内为你提供 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)