Clojure学习笔记(二)——序列

最近简单学习下Clojure,网上的资料也不是很多,自己写个小教程。

在Clojure中,很多数据结构都可以通过同一个抽象概念来访问:序列(Seq)。

可被视为序列的容器,被称为可序化的(seq-able,发音“SEEKabull”)。各种各样可序化的容器包括:

  • 所有的Clojure容器
  • 所有的Java容器
  • Java数组和字符串
  • 正则表达式的匹配结果
  • 目录结构
  • 输入/输出流
  • XML树

一切皆为序列

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

  • 你能够得到序列的第一个元素。
(first aseq)
  • 你能够获取第一个元素后面的一切东西,换句话说,就是序列的剩余部分。
(rest aseq)
  • 你可以通过向现有序列的前端添加元素,来创建一个新的序列。这就是所谓的cons。
(cons elem aseq)

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

(seq coll)

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

(next aseq)

(next aseq)等价于 (seq (rest aseq))。

映射表和集合的遍历顺序是稳定的,但这个顺序取决于具体的实现细节。

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

(sorted-set& elements)

sorted-map来创建一个有序的映射表。

(sorted-map& elements)

conj 会向容器添加一个或是多个元素,into 则会把容器中的所有元素添加至另一个容器。添加数据时,conj和into都会根据底层数据结构的特点选取最高效的插入点。

  • 对于列表而言,conj和into会在其前端进行添加。

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

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

Clojure序列是不可变的:它们永远都不会发生变化。所以我们可以很容易的就做出推断:Clojure序列在并发访问时是安全的。

序列库

Clojure的序列库包含以下四种函数:

  • 创建序列的函数。
  • 过滤序列的函数。
  • 序列谓词。
  • 序列转换函数。

创建序列

ranage会生成一个从start开始到end结束的序列,每次的增量为step。

(range start? end step?)

范围包含了start,但并不包含end。如果你没有指定的话,start默认为0,step默认为1。

user=> (range 10)
(0 1 2 3 4 5 6 7 8 9)
user=> (range 10 20)
(10 11 12 13 14 15 16 17 18 19)
user=> (range 1 10 2)
(1 3 5 7 9)

repeat函数会重复n次元素x。当只传入一个参数时,repeat会返回一个惰性的无限序列。

(repeat n x)
user=> (repeat 5 1)
(1 1 1 1 1)
user=> (repeat 6 "x")
("x" "x" "x" "x" "x" "x")

iterate起始于值x,并持续地对每个值应用函数f,以计算下一个值,直至永远。

(iterate f x)

take会返回一个包含了容器中前n项元素的惰性序列,这就提供了一种在无限序列上创建有限视图的途径。

(take n sequence)

cycle函数接受一个容器,并无限的对其进行循环。

(cycle coll)

interleave函数接受多个容器作为参数,并产生一个新的容器,这个新容器会从每个参数容器中交错地提取元素,直至其中某个容器元素被耗尽。

(interleave& colls)

当其中的某个容器被耗尽时,interleave就会终止。所以,你可以把有限容器与无限容器混合到一块儿。

与interleave密切相关的interpose函数,把输入序列中的每个元素用分隔符隔开,并作为新的序列返回。

(interpose separator coll)

interpose和(apply str ...)的结合,正好可以用来产生输出字符串。

作为一种惯用法,(apply str ...)是如此的常用,以至于Clojure甚至专门把它封装为clojure.string/join。

(join separator sequence)

对应每种Clojure中的容器类型,都有一个可以接受任意数量参数的函数,用来创建该类型的容器。

(list_& elements)
(vector_& elements)
(hash-set_& elements)
(hash-map key-1 val-1 ...)

过滤序列

filter接受一个谓词和一个容器作为参数,并返回一个序列,这个序列的所有元素都经谓词判定为真。

(filter pred coll)
user=> (filter even? '(1, 2, 3, 4, 5))
(2 4)

你还可以使用take-while从序列中截取开头的一段,其每个元素都被谓词判定为真。

(take-while pred coll)

drop-while 从序列的起始位置开始,逐个丢弃元素,直至谓词判定为真,然后返回序列剩余的部分。

(drop-while pred coll)

split-at和split-with能把一个容器一分为二。

(split-at index coll)
(split-with pred coll)

split-at接受一个索引作为参数,而split-with则接受一个谓词。

user=> (split-at 5 (range 10))
[(0 1 2 3 4) (5 6 7 8 9)]
user=> (split-with #(<= % 10) (range 0 20 2))
[(0 2 4 6 8 10) (12 14 16 18)]

所有take-、split-和drop-打头的函数,返回的都是惰性序列。

序列谓词

序列谓词会要求其他谓词应如何判定序列中的每一个元素。例如,every?要求其他谓词对序列中的每个元素都必须判定为真。

(every? pred coll)
user=> (every? odd? [1 3 5])
true
user=> (every? odd? [1 2 5])
false

只要有一个元素被谓词判定为非假,some就会返回这个值,如果没有任何元素符合,则some返回nil。

(some pred coll)
user=> (some even? [1 2 3])
true
user=> (some even? [1 3 5])
nil

注意,some没有以问号结尾。尽管总被当作谓词使用,但some并非谓词。因为 some 返回的是第一个符合项的值,而非 true。

其他谓词从名称就能很明显的表现出其行为。

(not-every? pred coll)
(not-any? pred coll)

序列转换

转换函数用于对序列中的值进行转换。最简单的转换是映射函数map。

(map f coll)

map接受一个源容器coll和一个函数f作为参数,并返回一个新的序列。该序列的所有元素,都是通过对coll中的每个元素调用f得到的。

还可以传入多个容器给map。在这种情况下,f必须是一个多参函数。map会从每个容器分别取出一个值,作为参数来调用f,直到数量最少的那个容器被耗尽为止。

另一个常用的转换是归纳函数reduce。

(reduce f coll)

其中f是一个接受两个参数的函数。reduce首先用coll的前两个元素作为参数来调用f,然后用得到的结果和第三个元素作为参数,继续调用f,以此类推。

user=> (reduce + (range 1 11))
55

你可以使用sort或sort-by对容器进行排序。

(sort comp? coll)
(sort-by a-fn comp? coll)

sort 会依据元素的自然顺序对容器进行排序,sort-by 则会对每个元素调用 a-fn,再依据得到的结果序列来进行排序。

user=> (sort [42 1 6 11])
(1 6 11 42)
user=> (sort-by #(.toString %) [42 1 6 11])
(1 11 42 6)

如果不打算按照自然顺序排序,你可以为sort或sort-by指定一个可选的比较函数comp。

user=> (sort > [42 1 6 11])
(42 11 6 1)

惰性和无限序列

大多数Clojure序列都是惰性的,换句话说,直到真的需要时,元素才会被计算出来。使用惰性序列有很多好处。

  • 你可以推迟那些实际上并不需要的昂贵计算。
  • 你可以处理超出内存允许范围的庞大数据集。
  • 你可以将I/O推迟至确实需要时才进行。

当你在REPL中查看很大的序列时,你可以使用take来阻止REPL对整个序列进行求值。

在另外一些情况下,你遇到的问题可能正好相反。doall迫使Clojure遍历序列中的元素,并把这些元素作为结果返回。

(doall coll)

你还可以使用dorun。

(dorun coll)

dorun 同样会遍历容器中的元素,但它不会把穿过的元素保留在内存中。这样的结果是,dorun可以遍历那些超过了内存容许范围的超大容器。

调用特定于结构的函数

Clojure包含了一些特定目标的函数,面向列表、向量、映射表、结构和集合。

列表函数

Clojure支持peek和pop这两个名称比较传统的函数,分别用于取出列表的第一个元素,和其余的那些元素。

(peek coll)
(pop coll)
user=> (peek '(1 2 3))
1
user=> (pop '(1 2 3))
(2 3)

peek等同于first,但pop则与rest不同。如果是空序列,pop会抛出一个异常。

user=> (rest ())
()
user=> (pop ())
Execution error (IllegalStateException) at user/eval53 (REPL:1).
Can't pop empty list

向量函数

向量也支持peek和pop,但它们是从向量的末尾开始处理元素的。

user=> (peek [1 2 3])
3
user=> (pop [1 2 3])
[1 2]

get返回索引位置的值,倘若索引超出了向量边界,则返回nil。

user=> (get [:a :b :c] 1)
:b

向量自身也是函数。它接受一个索引作为参数并返回对应的值,或是当索引超过边界时,抛出一个异常。

user=> ([:a :b :c] 1)
:b

assoc会在指定的索引位置,关联一个新的值。

user=> (assoc [0 1 2 3 4] 2 :two)
[0 1 :two 3 4]

subvec会返回向量的一个子向量。

(subvec avec start end?)

若未指定end,则默认为向量的末尾。

当然,你也可以通过组合drop和take来模拟subvec。不同之处在于,take和drop非常通用,可用于任何序列。令一方面,对于向量而言,subvec要更快一些。每当你看到subvec这样特定于结构的函数,同时其功能又与序列库中的某个函数产生了重复时,那最有可能的理由就是性能。

映射表

Clojure提供了几个用于从映射表中读取键和值的函数。keys将所有的键作为序列返回,vals则将所有的值作为序列返回。

(keys map)
(vals map)

get会返回键对应的值,或者是返回nil。

(get map key value-if-not-found?)

有一种比用 get 更简单的方法。因为映射表同时也是一个函数,以它自己的键作为参数。

关键字同样也是函数。他接受一个容器作为参数,并在这个容器中查找其自身。

如果在映射表中查找一个键并返回了 nil,你无法确认究竟是键对应的值为nil,还是这个键在映射表中根本就不存在。contains?函数就可以解决这个问题,它只是单纯的检测某个键是否存住。

(contains? map key)

另外一种方法是调用get,它可以传入可选的第三个参数,如果键未能找到,那么就会返回这个参数的值。

Clojure还提供了几个会构建新映射表的函数。

  • assoc返回新增了一个键值对的映射表。
  • dissoc返回移除了某些键的映射表。
  • select-keys返回一个映射表,仅保留了参数传入的那些键。
  • merge可以合并映射表。如果多个映射表包含了同一个键,那么最右边的获胜。

最有趣的映射表构建函数是merge-with。

(merge-with merge-fn & maps)

merge-with与merge很类似,除了当两个或以上的映射表中有相同的键时,你能指定一个你自己的函数,来决定如何合并这个键对应的值。

集合函数

clojure.set函数执行由集合论而来的操作。

  • union返回的集合,包含了所有输入集合中的元素。
  • intersection返回的集合,其所有元素都曾同时出现于多个输入集合中。
  • difference 返回的集合,其所有元素都出现于第一个输入集合,但却未出现于第二个中。
  • select返回所有元素都能与给定谓词相匹配的一个集合。

rename函数可以用来给键(数据库的列)重新命名,基于一个映射表,把原来的名称改成新的。

(rename relation rename-map)

select 函数返回经谓词计算结果为真的那些映射表,这里的谓词与 SQL 语言中SELECT语句的WHERE部分非常类似。

(select pred relation)

project函数返回的那些映射表中,仅包含与参数匹配的键。

(project relation keys)

project与指定了列的子集的SQL SELECT语句非常类似。

你可能感兴趣的:(Clojure学习笔记(二)——序列)