3.2 使用序列库
Clojure序列库提供了一组丰富的函数,可用于任意序列。如果你的背景是来自被名词统治着的面向对象世界,那么序列库就真的可以被称作是“动词的复仇”了。这些函数提供了丰富的基础功能,任何遵循了基本first/rest/cons契约的数据结构都能从中获益。
接下来介绍的函数分为四大类。
● 创建序列的函数。
● 过滤序列的函数。
● 序列谓词。
● 序列转换函数。
这样来分类是有一些武断。由于序列是不可变的,所以大多数序列函数实际上都会创建新的序列。还有一些序列函数,同时兼具过滤和转换的功能。然而,这些分类毕竟提供了一份粗略的路线图,能在探索这个大型的函数库时提供帮助。
3.2.1 创建序列
除了序列字面量以外,Clojure还提供了若干函数用于创建序列。ranage会生成一个从start开始到end结束的序列,每次的增量为step。
(range start? end step?)
范围包含了start,但并不包含end。如果你没有指定的话,start默认为0,step默认为1。试试看在REPL中创建几个“范围”。
(range 10) -> (0 1 2 3 4 5 6 7 8 9) (range 10 20) -> (10 11 12 13 14 15 16 17 18 19) (range 1 25 2) -> (1 3 5 7 9 11 13 15 17 19 21 23)
repeat函数会重复n次元素x。
(repeat n x)
用REPL中尝试一下repeat。
(repeat 5 1) -> (1 1 1 1 1) (repeat 10 "x") -> ("x" "x" "x" "x" "x" "x" "x" "x" "x" "x")
range和repeat都表现出来了这样一种想法:它们可以无限延伸。你不妨把iterate就想成是range的无限扩展。
(iterate f x)
iterate起始于值x,并持续地对每个值应用函数f,以计算下一个值,直至永远。
如果你是用inc来进行迭代,并且起始于1,那么你就能生成所有的正整数。
(take 10 (iterate inc 1)) -> (1 2 3 4 5 6 7 8 9 10)
由于这是个无限序列,你需要另一个新函数,来帮助你在REPL中查看这个序列。
(take n sequence)
take会返回一个包含了容器中前n项元素的惰性序列,这就提供了一种在无限序列上创建有限视图的途径。
能包含全部整数的序列相当有用,让我们将其定义为函数,供将来使用。
(defn whole-numbers [] (iterate inc 1)) -> #'user/whole-numbers
当只传入一个参数时,repeat会返回一个惰性的无限序列。
(repeat x)
在REPL中尝试一下这种形式的repeat吧,不要忘了用take把结果包起来哦。
(take 20 (repeat 1)) -> (1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1)
cycle函数接受一个容器,并无限的对其进行循环。
(cycle coll)
在REPL中尝试对一些容器进行循环吧。
(take 10 (cycle (range 3))) -> (0 1 2 0 1 2 0 1 2 0)
interleave函数接受多个容器作为参数,并产生一个新的容器,这个新容器会从每个参数容器中交错地提取元素,直至其中某个容器元素被耗尽。
(interleave& colls)
当其中的某个容器被耗尽时,interleave就会终止。所以,你可以把有限容器与无限容器混合到一块儿。
(interleave (whole-numbers) ["A" "B" "C" "D" "E"]) -> (1 "A" 2 "B" 3 "C" 4 "D" 5 "E")
与interleave密切相关的interpose函数,把输入序列中的每个元素用分隔符隔开,并作为新的序列返回。
(interpose separator coll)
你可以使用interpose来创建,分隔的字符串。
(interpose "," ["apples" "bananas" "grapes"]) -> ("apples" "," "bananas" "," "grapes")
interpose和(apply str ...)的结合,正好可以用来产生输出字符串。
(apply str (interpose \, ["apples" "bananas" "grapes"])) -> "apples,bananas,grapes"
作为一种惯用法,(apply str ...)是如此的常用,以至于Clojure甚至专门把它封装为clojure.string/join。
(join separator sequence)
下面借助clojure.string/join,用逗号来对一个单词列表进行分隔。
(use '[clojure.string :only (join)]) (join \, ["apples" "bananas" "grapes"]) -> "apples,bananas,grapes"
对应每种Clojure中的容器类型,都有一个可以接受任意数量参数的函数,用来创建该类型的容器。
(list_& elements) (vector_& elements) (hash-set_& elements) (hash-map key-1 val-1 ...)
hash-set的近亲set与其工作方式稍有不同:set函数期望其第一个参数是个容器。
(set [1 2 3]) -> #{1 2 3}
而hash-set则接受可变的参数列表。
(hash-set 1 2 3) -> #{1 2 3}
vector也有一个近亲vec,vec接受容器作为参数,而非可变的参数列表。
(vec (range 3)) -> [0 1 2]
现在,你已经掌握了创建序列的基础知识。接下来,你可以使用一些其他的Clojure函数,来对序列进行过滤和转换。
3.2.2 过滤序列
Clojure提供了若干函数用来过滤序列,即由原序列得到一个子序列。这些函数中,最基本的是filter。
(filter pred coll)
filter接受一个谓词和一个容器作为参数,并返回一个序列,这个序列的所有元素都经谓词判定为真。你可以对上一节的whole-numbers进行过滤,得到奇数或是偶数。
(take 10 (filter even? (whole-numbers))) -> (2 4 6 8 10 12 14 16 18 20) (take 10 (filter odd? (whole-numbers))) -> (1 3 5 7 9 11 13 15 17 19)
你还可以使用take-while从序列中截取开头的一段,其每个元素都被谓词判定为真。
(take-while pred coll)
例如,为了从字符串中逐个获取第一个元音字符之前的所有非元音字符,可以如下这么做。
(take-while (complement #{\a\e\i\o\u}) "the-quick-brown-fox") -> (\t \h)
在这儿发生了两件有趣的事情。
● 集合同时也可作为函数。所以你可以把#{\a\e\i\o\u}读作“元音集”,或是“用于检测参数是否为元音的函数。”
● complement 会反转另一个函数的行为。前例的那个反转函数用于检测参数是不是一个元音。
与take-while相对的是drop-while函数。
(drop-while pred coll)
drop-while 从序列的起始位置开始,逐个丢弃元素,直至谓词判定为真,然后返回序列剩余的部分。你可以使用drop-while来丢弃字符串中起头的那些非元音字符。
(drop-while (complement #{\a\e\i\o\u}) "the-quick-brown-fox") -> (\e \- \q \u \i \c \k \- \b \r \o \w \n \- \f \o \x)
split-at和split-with能把一个容器一分为二。
(split-at index coll) (split-with pred coll)
split-at接受一个索引作为参数,而split-with则接受一个谓词。
(split-at 5 (range 10)) ->[(0 1 2 3 4) (5 6 7 8 9)] (split-with #(<= % 10) (range 0 20 2)) ->[(0 2 4 6 8 10) (12 14 16 18)]
当然,所有take-、split-和drop-打头的函数,返回的都是惰性序列。
3.2.3 序列谓词
过滤函数往往会接受谓词作为其参数,并返回新的序列。序列谓词与其密切相关。序列谓词会要求其他谓词应如何判定序列中的每一个元素。例如,every?要求其他谓词对序列中的每个元素都必须判定为真。
(every? pred coll)
(every? odd? [1 3 5])
-> true
(every? odd? [1 3 5 8])
-> false
some设置的门槛相对较低。
(some pred coll)
只要有一个元素被谓词判定为非假,some就会返回这个值,如果没有任何元素符合,则some返回nil。
(some even? [1 2 3]) -> true (some even? [1 3 5]) -> nil
注意,some没有以问号结尾。尽管总被当作谓词使用,但some并非谓词。因为some 返回的是第一个符合项的值,而非 true。由于 even?本身就是一个谓词,所以当some与even?配对使用时,其间的差异并不明显。为了体验非true匹配,让我们试着用some和identity一起来找出序列中的第一个非nil值吧。
(some identity [nil false 1 nil 2]) -> 1
其他谓词从名称就能很明显的表现出其行为。
(not-every? pred coll) (not-any? pred coll)
不是所有的正整数都是偶数。
(not-every? even? (whole-numbers)) -> true
但声称所有的正整数都是偶数则显然是一句谎言。
(not-any? even? (whole-numbers)) -> false
请注意,这里我们选择的问题都是事先就已经知道答案的。一般情况下,在对无限容器应用谓词时你要格外小心。它们可能会无止境的运行下去。
3.2.4 序列转换
转换函数用于对序列中的值进行转换。最简单的转换是映射函数map。
(map f coll)
map接受一个源容器coll和一个函数f作为参数,并返回一个新的序列。该序列的所有元素,都是通过对coll中的每个元素调用f得到的。你可以使用map把一个容器的每个元素都用HTML标记给包起来。
(map #(format "<p>%s</p>" %) ["the" "quick" "brown" "fox"]) -> ("<p>the</p>" "<p>quick</p>" "<p>brown</p>" "<p>fox</p>")
还可以传入多个容器给map。在这种情况下,f必须是一个多参函数。map会从每个容器分别取出一个值,作为参数来调用f,直到数量最少的那个容器被耗尽为止。
(map #(format "<%s>%s</%s>" %1 %2 %1) ["h1" "h2" "h3" "h1"] ["the" "quick" "brown" "fox"]) -> ("<h1>the</h1>" "<h2>quick</h2>" "<h3>brown</h3>" "<h1>fox</h1>")
另一个常用的转换是归纳函数reduce。
(reduce f coll)
其中f是一个接受两个参数的函数。reduce首先用coll的前两个元素作为参数来调用f,然后用得到的结果和第三个元素作为参数,继续调用f,以此类推。比如,你可以用reduce来让一些数字相加。
(reduce + (range 1 11)) -> 55
或者让它们相乘。
(reduce * (range 1 11)) -> 3628800
你可以使用sort或sort-by对容器进行排序。
(sort comp? coll) (sort-by a-fn comp? coll)
sort 会依据元素的自然顺序对容器进行排序,sort-by 则会对每个元素调用 a-fn,再依据得到的结果序列来进行排序。
(sort [42 1 7 11]) -> (1 7 11 42) (sort-by #(.toString %) [42 1 7 11]) -> (1 11 42 7)
如果不打算按照自然顺序排序,你可以为sort或sort-by指定一个可选的比较函数comp。
(sort > [42 1 7 11]) -> (42 11 7 1) (sort-by :grade > [{:grade 83} {:grade 90} {:grade 77}]) -> ({:grade 90} {:grade 83} {:grade 77})
所有过滤和转换的祖先都是列表解析(list comprehension)。列表解析使用集合记号法(set notation),基于一个已存在的列表来创建新的列表。换句话说,解析式描述了结果列表必须满足的性质。一般情况下,一个列表解析会包含以下内容。
● 输入列表。
● 输入列表中元素所对应的占位变量。
● 作用于元素的谓词。
● 一个输出形式,它负责基于那些满足谓词要求的列表元素来产生输出。
当然,Clojure把列表解析的概念泛化为了序列解析(sequence comprehension)。在Clojure中,是使用for宏来进行解析的。
(for [binding-form coll-expr filter-expr? ...] expr)
for 接受一个向量作为参数,该参数是由一系列 binding-form/coll-expr 和可选的filter-expr组成的,然后是依据表达式expr来产生结果序列。
列表解析比诸如 map 和filter 这样的函数更加通用,而且,事实上它可以模拟之前的大多数过滤和转换函数。
你可以用列表解析来重写之前那个map的例子。
(for [word ["the" "quick" "brown" "fox"]] (format "<p>%s</p>" word)) -> ("<p>the</p>" "<p>quick</p>" "<p>brown</p>" "<p>fox</p>")
这样读起来几乎就像是英语一样:“For [each] word in [a sequence of words] format [according to format instructions]”。即按照格式要求,对一系列单词逐个进行格式化。
借助:when子句,解析也可以用来模拟filter函数。你可以把even?传给:when,用来过滤偶数。
(take 10 (for [n (whole-numbers) :when (even? n)] n)) -> (2 4 6 8 10 12 14 16 18 20)
只要:while字句的表达式保持为真,它就会继续进行求值。
(for [n (whole-numbers) :while (even? n)] n) -> ()
当你有多个绑定表达式时,for才会真正地发挥其威力。例如,你可以通过绑定rank和file,用代数计数法来表示棋盘上的所有位置。
(for [file "ABCDEFGH" rank (range 1 9)] (format "%c%d" file rank)) -> ("A1" "A2" ...已省略... "H7 ""H8")
序列解析中,Clojure会从按照右到左的顺序,来遍历绑定表达式。由于在绑定形式中,rank列于file的右手边,所以rank会迭代的更快。如果你希望让file迭代的更快,可以反转绑定的顺序,把rank列在第一位。
(for [rank (range 1 9) file "ABCDEFGH"] (format "%c%d" file rank)) -> ("A1" "B1" ...已省略... "G8" "H8")
在很多语言中,转换、过滤和解析都是立即执行的。但千万不要认为Clojure也是如此。绝大多数的序列函数都不对元素进行遍历,除非你真的尝试要去使用它们。