- Clojure程序设计
- (美)Stuart Halloway Aaron Bedra
- 2270字
- 2020-06-26 14:08:19
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的更改过的拷贝。”