03. 小火汁,没想到你也是序列
在上一节中,我们粗略的概览了
在本章节的最后,我们会动手实现两个函数——一个
一切皆序列?
上一节我们提到的字符串、列表、向量、映射表、集合之间并不是毫无关联的——它们有一个共同的抽象:序列(seq
(class (seq "abc"))
;=> clojure.lang.StringSeq
(class (seq [1 2 3]))
;=> clojure.lang.PersistentVector$ChunkedSeq
序列并没有底层的实现,而是一个抽象的接口。序列拥有三个基本操作:
- (first seq) ——返回序列的第一项
- (**rest***seq*) ——返回除第一项以外的余项
- (cons elem seq) ——返回一个新序列,将
elem 添加到seq 的前面
(def nums [1 2 3])
(first nums) ;=> 1
(rest nums) ;=> (2 3)
(cons 0 nums) ;=> (0 1 2 3)
可以看到,任何一个可以切割成头尾两部分的对象都可以是序列。我们再举几个序列的例子:
- 一个字符串(字符序列)
- 孙悟空的妖怪女朋友们(谢罪序列)
- 野兽先辈是女生的
114514 种假说(恶 臭 序 列) - 全体自然数的集合(无限长序列)
- 系统中的状态变化(惰性序列)
可以看到,序列是一个非常抽象的概念,除了能够表示像向量这样的集合以外,序列还能表示各种存在先后顺序的事物。我们将一个可以转化为序列的结构称作可序化结构。
有了序列我们就可以实现各种通用的算法,而不用针对每种特定的序列单独实现了。
值得一提的是,对空列表、空向量等对象调用
;无法正常工作的my-empty
(defn my-empty? [xs] (if xs true false))
;=> #'user/my-empty?
(my-empty? [])
;=> true
;使用seq修复这个bug
(defn my-empty? [xs] (if (seq xs) true false))
;=> #'user/my-empty?
(my-empty? [])
;=> false
将一个复合结构转化成
高阶函数
- 接受一个函数作为参数的函数
- 返回一个函数作为返回值的函数
我们在
使用高阶函数操作数据结构已经被证明是一种行之有效的方法。包括
在过程式的编程思想中,我们通常使用循环来操作数据结构。下面看几个
// 列表求和
public static int sum(List<Integer> ints) {
int acc = 0;
for (int n : ints) {
acc += n;
}
return acc;
}
// 找出列表中所有的正整数
public static List<Integer> filterPos(List<Integer> ints) {
List<Integer> temp = new ArrayList<>();
for (int n : ints) {
if (n > 0) {
temp.add(n);
}
}
return temp;
}
// 将列表中所有的整数翻倍
public static List<Integer> doubled(List<Integer> ints) {
List<Integer> temp = new ArrayList<>();
for (int n : ints) {
temp.add(2 * n);
}
return temp;
}
下面让我们找出循环中存在的问题,然后循序渐进的过渡到高阶函数的世界。
我们先从
List<Integer> ints = Arrays.asList(1, 2, 3, 4);
System.out.println(sum(ints));
//-> 10
但现在我们又有了新的需求,我们要对这个列表进行累乘。对于循环来说,我们没有办法,只好重新写一个新的静态方法:
public static int product(List<Integer> ints) {
int acc = 1;
for (int n : ints) {
acc *= n;
}
return acc;
}
现在让我来描述一下我是如何实现
你或许已经意识到了:
让我们把视线转回
(defn sum [xs] (reduce + 0 xs))
(defn product [xs] (reduce * 1 xs))
(def ints [1 2 3 4])
(sum ints) ;=> 10
(product ints) ;=> 24
(defn myreduce [fn acc xs]
(if-let [[head & tail] (seq xs)]
(recur fn (fn acc head) tail)
acc))
(myreduce + 0 [1 2 3 4]) ;=> 10
- 如果不为
nil ,则对seq 进行解构,分成头尾两部分。之后将seq 的第一项(头部)和acc 使用函数fn 结合,然后对myreduce 进行递归。 - 如果为
nil ,说明容器中所有的元素都已经消耗完毕,此时acc 就是我们求得的结果了。
通过
现在再来看看那个循环写法的例子,
(defn filter-pos [xs]
(filter pos? xs))
(defn doubled [xs]
(map #(* % 2) xs))
(filter-pos [1 -1 -2 3 4]) ;=> (1 3 4)
(doubled [1 2 3 4]) ;=> (2 4 6 8)
来看看
(defn mymap [f xs]
(seq (reduce #(conj %1 (f %2)) [] xs)))
(defn myfilter [pred xs]
(seq (reduce #(if (pred %2) (conj %1 %2) %1) [] xs)))
(mymap str [1 2 3]) ;=> ("1" "2" "3")
(myfilter pos? [1 -1 2 -2]) ;=> (1 2)
public static int sum(List<Integer> ints) {
int acc = 0; // ①
for (int n : ints) {
acc += n;// ②
}
return acc;
}
public static List<Integer> doubled(List<Integer> ints) {
List<Integer> temp = new ArrayList<>(); // ①
for (int n : ints) {
temp.add(2 * n); // ②
}
return temp;
}
比较一下两处 ① 和两处 ②,我们可以发现,
在
(conj [1 2 3] 4) ;=> [1 2 3 4]
!注:
因此,我们可以使用
;拷贝列表
(reduce conj [] [1 2 3]) ;=> [1 2 3]
;反转列表
(reduce #(cons %2 %1) [] [1 2 3]) ;=> (3 2 1)
而
太多括号了?
现在让我们实现这样一个需求:
- 输入一个整数列表
- 去掉其中的负数
- 将每一项加一
- 最后求和
我们可以活用刚才学到的高阶函数的知识:
(defn filter-pos-inc-sum [xs]
(reduce + 0
(map inc
(filter pos? xs))))
(filter-pos-inc-sum [1 2 -1 -2 3]) ;=> 9
emmm,这个函数确实工作的很好,但是可读性不佳——首先,括号嵌套了太多层,显得不太优雅。其次,
(defn filter-pos-inc-sum [xs]
(->> xs
(filter pos?)
(map inc)
(reduce + 0)))
(filter-pos-inc-sum [1 2 -1 -2 3]) ;=> 9
除了操作数据结构以外,
(->> 1114514
(+ 1919)
(/ 810))
;=> 90/12930
(-> 25 Math/sqrt int inc str) ;=> "6"
序列操作一览
了解了高阶函数的概念之后,让我们来看看
构造序列
除了最常用的
(range start? end step?)
(range 5) ;=> {0 1 2 3 4)
(range 2 10) ;=> (2 3 4 5 6 7 8 9)
(range 0 10 2) ;=> (0 2 4 6 8)
(repeat "+1") ; 无限+1,就和你的复读机群友一样
;=> ...永远不会停下
(repeat 3 "hello")
;=> ("hello" "hello" "hello")
(defn rand-int-seq [max]
(repeatedly #(rand-int max)))
(->> (rand-int-seq 10)
(map #(+ % 10)) ;=>随机数取值范围为[10, 20)
(take 10)
)
(iterate fnext seed)
使用
- 序列的首项为
seed - 设序列的低
n-1 项为x ,那么序列的第n 项为(fnext x)
在之前的章节中我们已经试过使用
(def fib
(letfn [(fnext [[a b]] [b (+ a b)])] ;定义局部函数
(->> (iterate fnext [0 1])
(map first))))
(take 10 fib)
;=> (0 1 1 2 3 5 8 13 21 34)
这段代码不是特别直观,我们先将
(take 10
(iterate
(fn [[a b]] [b (+ a b)])
[0 1]))
;=> ([0 1] [1 1] [1 2] [2 3] [3 5] [5 8] [8 13] [13 21] [21 34] [34 55])
可以看到,
之所以这么实现是因为计算
(def zero-one [0 1])
(take 10 (cycle zero-one))
;=> (0 1 0 1 0 1 0 1 0 1)
;字母表序列
(defn range-char [begin end]
(map char
(range (int begin) (inc (int end)))))
(def charset
(concat
(range-char \A \Z)
(range-char \a \z)))
charset
;=> (\A \B \C ...\Z \a \b \c ...\z)
(interleave natural-nums charset)
;=> (0 \A 1 \B 2 \C ... 49 \x 50 \y 51 \z)
如你所见,
(defn philosophize [s]
(apply str (interpose \♂ s)))
(philosophize "幻想乡")
;=> "幻♂想♂乡"
类似的还有使用分隔符连接字符串的
(join " " ["Hello" "World"])
;=> "Hello World"
每一种
(list & elems)
(vector & elems)
(hash-set & emels)
(hash-map & keys-values)
通过
(apply hash-map [:a 1 :b 2 :c 3])
{:c 3, :b 2, :a 1}
(set [:a :b :c])
;=> #{:c :b :a}
(vec {:a 1, :b 2, :c 3})
;=> [[:a 1] [:b 2] [:c 3]]
过滤、截取序列
(remove even? (range 10))
;=> (1 3 5 7 9)
(take 3 natural-nums)
;=> (0 1 2)
(take-while #(< % 10) natural-nums)
;=> (0 1 2 3 4 5 6 7 8 9)
drop、
(drop 5 (range 10))
;=> (5 6 7 8 9)
(split-at 5 (range 10))
;=> [(0 1 2 3 4) (5 6 7 8 9)]
(split-with #(< % 3) (range 10))
;=> [(0 1 2) (3 4 5 6 7 8 9)]
序列谓词
序列谓词用于判断序列是否满足一定的条件。
(every? pos? [0 1 2]) ;=> true
(every? pos? [-1 1 2]);=> false
(some pos? [-1 1 2]) ;=> true
(some pos? [-1 -2 0]) ;=> nil
注意,
;identity总是返回其参数本身
(some identity [nil false 1 2 3])
;=> 1
实际上
序列变换
常用的序列变换操作
(defn uncompress [coll]
(mapcat (fn [[x n]] (repeat n x)) coll))
(uncompress [[:a 2] [:b 3] [:c 4]])
;=> (:a :a :b :b :b :c :c :c :c)
有趣的是,
(defn mymap [f coll]
(mapcat #(cons (f %) nil) coll))
(defn myfilter [pred coll]
(mapcat (fn [x] (if (pred x) (cons x nil) nil)) coll))
(sort [4 1 3 2])
;=> (1 2 3 4)
(sort > [4 1 3 2])
;=> (4 3 2 1)
(def scores [
{:name "Khellendros", :score 100}
{:name "KMR", :score 114514}
{:name "Jack", :score 80}
])
(sort-by :score > scores)
;=> ({:name "KMR", :score 114514} {:name "Khellendros", :score 100} {:name "Jack", :score 80})
上面的代码会根据每个人的成绩降序排列
(group-by identity [:a :a :b :c :b])
;=> {:a [:a :a], :b [:b :b], :c [:c]}
(partition 2 (range 10))
;=> ((0 1) (2 3) (4 5) (6 7) (8 9))
计数函数count-map
我们先从一个简单的例子开始:统计序列中相同的元素的数量:
(count-map [:a :b :a :c :a :b])
;=> ([:a 3], [:b 2], [:c 1])
我们可以使用
(defn count-map [coll]
(->> coll
(group-by identity)
(map (fn [[k vs]] [k (count vs)]))))
压缩函数compress
当我们需要处理大量数据时,最好先对数据进行压缩在进行运算。
(compress [:a :a :a :a :b :b :b :c :a :a])
;=> (:a 4 :b 3 :c 1 :a 2)
当我们需要将多个元素压缩成
- 前一个元素的值
- 前一个元素已累积的数量
- 已完成压缩的序列
有了思路之后我们就能写下
;x-前一个元素
;n-前一个元素已积攒的l数量
;acc-已完成压缩的序列
;elem-当前元素
(defn cmpr [[x n acc] elem]
(if (= x elem)
[x (inc n) acc] ;如果x与elem相等,增加n的值
[elem 1 (conj acc x n)] ;否则开始积攒elem,将x和n加入已完成压缩序列
))
显而易见的,
注意,当
(defn end-reduce [[x n acc]] (conj acc x n ))
另外记得结果序列的前两项是
将以上内容整合起来,
(defn compress [coll]
(letfn [
(cmpr [[x n acc] elem]
(if (= x elem)
[x (inc n) acc]
[elem 1 (conj acc x n)]))
(end-reduce [[x n acc]] (conj acc x n))]
(->> coll
(reduce cmpr [nil 0 []])
end-reduce
(drop 2)))