3.1 一切皆序列

每一种聚合的(aggregate)数据结构,在Clojure中都能被视为序列。序列具有三大核心能力。

● 你能够得到序列的第一个元素。

        (first aseq)

如果参数aseq是空的或者是nil,则first返回nil。

● 你能够获取第一个元素后面的一切东西,换句话说,就是序列的剩余(rest)部分。

        (rest aseq)

如果没有更多的项,则rest返回一个空序列(而不是nil)。

● 你可以通过向现有序列的前端添加元素,来创建一个新的序列。这就是所谓的cons。

        (cons elem aseq)

在 Clojure 内部,这三个功能是在 Java 接口 clojure.lang.ISeq 中声明的。在阅读Clojure代码时尤其要记住这一点,因为名称ISeq常被用来与seq进行互换。

seq函数会返回一个序列,该序列源自任何一个可序化的其他容器。

        (seq coll)

如果coll是空的或者是nil,则seq返回nil。next函数也会返回一个序列,该序列由除第一个元素以外的其他所有元素组成。

        (next aseq)

(next aseq)等价于 (seq (rest aseq))。表3-1“澄清rest和next”阐明了rest与next的行为方式。

表3-1 澄清rest和next

如果你具有Lisp背景,想必已经料到这些序列函数能作用于列表。

        (first '(1 2 3))
        -> 1
        (rest '(1 2 3))
        -> (2 3)
        (cons 0 '(1 2 3))
        -> (0 1 2 3)

Clojure中,还是这些函数,对其他数据结构也同样有效。你可以把向量作为序列。

        (first [1 2 3])
        -> 1
        (rest [1 2 3])
        -> (2 3)
        (cons 0 [1 2 3])
        -> (0 1 2 3)

当你对向量使用rest或cons时,得到的结果是一个序列,而非向量。就像你从前面的输出中看到的那样,在REPL中,序列被打印出来以后,就好象是个列表一样。你可以用class函数来获取它的类,检查其实际的返回类型。

        (class (rest [1 2 3]))
        -> clojure.lang.PersistentVector$ChunkedSeq

类名末尾的那个$ChunkedSeq是Java为了对内联类进行名称改编,而采取的方式。你从某个特定容器类型产生出来的序列,总会被实现为ChunkedSeq,并内联到原始容器类里(此例中是PersistentVector)。

Cons的起源

Clojure序列是一种以Lisp实体列表为基础的抽象概念。在Lisp最初的实现中,有三个基本的列表操作分别名为:car、cdr和cons。car和cdr是首字母缩写,涉及最初IBM 704平台上的Lisp实现细节。不过包括Clojure在内的许多Lisp方言,都把这两个玄奥的名称替换为更有意义的名称:first和rest。

这第三个函数cons,是construct的简写。Lisp程序员把cons同时用作名词、动词和形容词。你可以使用cons创建一种被称为cons cell的数据结构,或者就将其简称为cons。

包括Clojure在内的大多数Lisp,保留了cons这个最初的名称。这是因为“construct”对于表示cons的用途而言,是一个相当不错的助记符。这也有助于提醒你,序列是不可变的。方便起见,你也可以说cons给序列添加了一个元素,但更准确的说法还是cons构建了一个新的序列。这个新序列与原来的那个相似,只不过新增了一个元素。

尽管序列的泛化及其强大,但有时你也会想要直接处理某种特定的实现类型。相关内容参见第3.5节“调用特定于结构的函数”。

如果你认为键值对也可算作是序列的元素,那么映射表也可以作为序列。

        (first {:fname "Aaron" :lname "Bedra"})
        -> [:lname "Bedra"]
        (rest {:fname "Aaron" :lname "Bedra"})
        -> ([:fname "Aaron"])
        (cons [:mname "James"] {:fname "Aaron" :lname "Bedra"})
        -> ([:mname "James"] [:lname "Bedra"] [:fname "Aaron"])

你也可以把集合当作序列。

        (first #{:the :quick :brown :fox})
        -> :brown
        (rest #{:the :quick :brown :fox})
        -> (:quick :fox :the)
        (cons :jumped #{:the :quick :brown :fox})
        -> (:jumped :brown :quick :fox :the)

为什么执行函数时传入的是向量,却返回了列表?

当你在REPL中尝试执行示例时,rest和cons的结果总是被显式为列表,甚至输入的是向量、映射表和集合时也是如此。这是否意味着Clojure会在内部把所有东西都转换成列表了呢?答案是,不!无论输入的是什么,序列函数返回的总是序列。你可以通过检查返回对象的Java类型来验证这一点。

            (class '(1 2 3))
            -> clojure.lang.PersistentList
            (class (rest [1 2 3]))
            -> clojure.lang.PersistentVector$ChunkedSeq

如你所见,(rest [1 2 3])的结果是某种类型的序列,而非列表。那么,为什么结果看起来会是个列表呢?

答案就在REPL。当你要求REPL显示一个序列时,它仅知道那是一个序列。这个序列究竟是用哪种类型的容器构建的,REPL一无所知。因此,它干脆就采用相同的方式来打印所有序列:遍历整个序列,并将其作为一个列表打印出来。

映射表和集合的遍历顺序是稳定的,但这个顺序取决于具体的实现细节,所以你不应该依赖它。比如,集合的元素不一定会依照你存放的顺序返回。

        #{:the :quick :brown :fox}
        -> #{:brown :quick :fox :the}

你如果想要可靠的顺序,可以用这个。

        (sorted-set& elements)

sorted-set会依据自然顺序对值进行排序。

        (sorted-set :the :quick :brown :fox)
        -> #{:brown :fox :quick :the}

同样,映射表键值对也不一定按照你存放的顺序返回。

        {:a 1 :b 2 :c 3}
        -> {:a 1, :c 3, :b 2}

你可以使用sorted-map来创建一个有序的映射表。

        (sorted-map& elements)

sorted-map也不会按照你存放的顺序返回,但它会根据键来进行排序。

        (sorted-map :c 3 :b 2 :a 1)
        -> {:a 1, :b 2, :c 3}

除了上述几个序列的核心函数,还有两个函数也值得马上介绍一下,它们是conj和into。

        (conj coll element & elements)
        (into to-coll from-coll)

conj 会向容器添加一个或是多个元素,into 则会把容器中的所有元素添加至另一个容器。添加数据时,conj和into都会根据底层数据结构的特点选取最高效的插入点。对于列表而言,conj和into会在其前端进行添加。

        (conj '(1 2 3) :a)
        -> (:a 1 2 3)
        (into '(1 2 3) '(:a :b :c))
        -> (:c :b :a 1 2 3)

而对于向量,conj和into则会把元素添加至末尾。

        (conj [1 2 3] :a)
        -> [1 2 3 :a]
        (into [1 2 3] [:a :b :c])
        -> [1 2 3 :a :b :c]

因为 conj(及其相关函数)会针对底层数据结构高效的进行操作,所以你总是能编写既高效又与底层特定实现完全解耦的代码。

Clojure 序列库特别适合于那些庞大的(甚至是无限的)序列。绝大多数 Clojure序列都是惰性的:只有当确实需要时,它们才真正的把元素生成出来。因此,Clojure序列函数能够处理那些无法驻留在内存中的超大序列。

Clojure序列是不可变的:它们永远都不会发生变化。所以我们可以很容易的就做出推断:Clojure序列在并发访问时是安全的。的确如此。然而,对人类语言来说,这也惹来了一个小麻烦。在描述那些可变的事物时,语言会显得更加顺畅。不妨考虑一下对下面这个假想序列函数triple的两种描述。

● triple会把序列中的每个元素分别乘与三。

● triple 接受一个序列,并返回一个新的序列,这个新序列的每个元素,都是原序列元素的三倍。

后一个版本具体并且准确。前者是更容易阅读一些,但可能会导致这个错误的印象:序列实际上改变了。莫要被愚弄了,序列永不改变。如果你看到这样的说法:“foo改变了x”,内心中应该这样来解读:“foo返回了一个x的更改过的拷贝。”