最近简单学习下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语句非常类似。
网友评论