summaryrefslogtreecommitdiff
path: root/content/article
diff options
context:
space:
mode:
authoreltrac <hi@eltr.ac>2026-09-26 18:22:05 +0800
committereltrac <hi@eltr.ac>2026-09-26 18:22:05 +0800
commit508fc446fb928794a37eff9802b11e0d01cc5ea7 (patch)
tree4e081bcbe299067643dcf1ed37e9bd9b92c49bfe /content/article
parent8d691defd8211d2a801eab9ad24e1f27816dc28c (diff)
[publish] /article/clj3
Diffstat (limited to 'content/article')
-rw-r--r--content/article/clj3.md571
1 files changed, 524 insertions, 47 deletions
diff --git a/content/article/clj3.md b/content/article/clj3.md
index 80475c32..ef22fe4b 100644
--- a/content/article/clj3.md
+++ b/content/article/clj3.md
@@ -5,18 +5,23 @@ slug = 'clj3'
aliases = '/article/tutorial-clj-3'
tags = ["Lisp","编程"]
categories = '茶歇蔻芥'
-date = 2026-09-25T20:17:41+08:00
-draft = true
+date = 2026-09-26 18:20:05
+draft = false
toc = true
+fedi = 'https://akk.eltr.ac/objects/fab1fbe1-dc87-4a2e-8c5a-fc9ab9942c55'
+++
-[上一篇](/article/tutorial-clj-2)我们讨论了不同类型的形式,了解了函数、Lambda、宏和特殊形式的皮毛,也学会了用一些基本的运算符、`let` 和 `if` 等等。今天的文章要讨论集合数据类型。不过在此之前,我们还是先看看普通的数据类型有哪些吗?<!--more-->
+[上一篇](/article/tutorial-clj-2)我们讨论了不同类型的形式,了解了函数、Lambda、宏和特殊形式的皮毛,也学会了用一些基本的运算符、`let` 和 `if` 等等。今天的文章要讨论集合数据类型。不过在此之前,我们还是先看看普通的数据类型有哪些。<!--more-->
+
+今天的文章有点长,所以读的时候,呃…… 准备一杯大号的咖啡?
## 你已经知道和你可能不知道的
和大部分编程语言一样,Clojure 支持数字、浮点数、字符串和布尔值。
-由于 Clojure 是运行在 JVM 上的,所以 Java 这些数据类型实际上对应一个 Java 类。数字是 `Long` 而不是 `Int`,前者能表示的最大数是 2 的 63 次方减 1,也就是有 64 位,第一位是符号位,后者是 32 位。和 Java 一样,Clojure 没有无符号数(unsigned int)。
+由于 Clojure 是运行在 JVM 上的,所以这些数据类型都对应一个 Java 类。数字是 `Long` 不是 `Int`,前者能表示的最大数是 2 的 63 次方减 1,也就是有 64 位,第一位是符号位,后者是 32 位。
+
+和 Java 一样,Clojure 没有无符号数(unsigned int)。
Clojure 还能在这些类型上调用 Java 的方法,比如 `String.getBytes()`。
@@ -27,7 +32,7 @@ Clojure 还能在这些类型上调用 Java 的方法,比如 `String.getBytes(
;; => java.lang.Byte
```
-Clojure 也没有用更小的 `Float`,也是用了双精度浮点数 `Double`。
+Clojure 也没有用更小的 `Float`,而是用了双精度浮点数 `Double`。
```clojure
(type 1.1)
@@ -45,10 +50,73 @@ Clojure 是门动态类型语言,一般不需要考虑太多类型问题,不
关键词类似字符串,不过是以 `:` 开头,中间不能有空格。其实他更像其他语言里的枚举类型(enums),但无需提前定义。关键词不能像字符串一样做裁剪、找子串、当作字符数组遍历之类的操作,就是一个整体。
-关键词可以用在大部分需要枚举类型的场合,也用来表示映射(map)里的键(key)。
+必要的时候,可以用 `name` 函数将它转换为字符串。
+
+```clojure
+(name :keyword)
+;; => "keyword"
+```
+
+关键词可以用在大部分需要枚举的场合,也用来表示映射里的键(key)。
其他的非集合类型还有我们上一篇讨论过的符号(Symbol)。
+## 集合数据类型与序列类型
+
+集合数据类型统称 *Collections*,一般缩写做 `coll`,你会在 Clojure 的文档和各种函数里见到这个缩写。要判断一个数据是否是集合,可以用谓词函数 `coll?`:
+
+```clojure
+(coll? 1) ;; => false
+(coll? "string") ;; => false
+
+(coll? '(1 2 3)) ;; => true
+(coll? [1 2 3]) ;; => true
+(coll? {:key "value"}) ;; => true
+(coll? #{1 2 3}) ;; => true
+
+;; 当然,代码即数据
+(coll? '(println "Hello World")) ;; => true
+```
+
+你之后还会见到很多 `?` 结尾的谓词函数。谓词函数(predicate)是指返回布尔值的函数,在编写自己的函数时,最好也按照这种规则命名。这就像…… Java 的 `IsXXX()` 方法一样。
+
+上面见到的四种有不同括号标记的集合数据类型就是今天的主角。在正式介绍他们之前,我还想讲讲序列(sequence)。序列和集合分别对应两个 Java 接口:`ISeq` 和 `IPersistentCollection`。下面是 `ISeq` 的定义:
+
+```java
+public interface ISeq extends IPersistentCollection {
+ Object first();
+ ISeq next();
+ ISeq more();
+ ISeq cons(Object o);
+}
+```
+
+`ISeq` 继承了 `IPersistentCollection`,可以理解为:序列是集合的子集。可以看到这个接口里的访问方法只有 `first()`、`next()` 和 `more()`,也就是说序列只支持**线性访问**,不能**随机访问**,也就是没有索引(index)和键(key)。
+
+可以用 `seq?` 判断一个集合数据是不是序列。
+
+```clojure
+(seq? '(1 2 3)) ;; => true
+(seq? [1 2 3]) ;; => false
+(seq? {:key "value"}) ;; => false
+(seq? #{1 2 3}) ;; => false
+```
+
+只有列表是序列,不过其他数据类型都能转换为序列。
+
+```clojure
+;; 没有 ? 的 seq 用于把集合转换为序列
+(seq [1 2 3]) ;; => (1 2 3)
+(seq {:key "value"}) ;; => ([:key "value"])
+(seq #{1 2 3}) ;; => (1 3 2)
+;; 字符串可以变成字符序列
+(seq "hello") ;; => (\h \e \l \l \o)
+```
+
+只有序列才能够遍历,Clojure 中许多集合相关的操作都会**自动**把集合数据转换为序列。注意看,映射被转换为序列后,每个元素都是一个包含键和值的向量,所以之后我们遍历映射的时候,其实是在遍历向量序列。
+
+序列并不是具体的数据类型,集合(collection)也不是,它们是对列表、向量、映射、集(set)以及惰性序列(lazy seq)的抽象。只不过许多操作都基于抽象的类型进行,这样很多函数就能通用。
+
## 圆括号和列表
我们在之前就见过列表(list),最普通的集合数据类型。
@@ -86,7 +154,7 @@ Clojure 是门动态类型语言,一般不需要考虑太多类型问题,不
![](https://img.eltr.ac/geedeapro/article/common-lisp-start/Cons-cells.svg.png)
{.dark:invert}
-虽然 Clojure 的 `cons` 也可以用相同的方式构造列表,但 *cons* 对却不存在。`cons` 的第二个函数只能是序列,不能是原子。
+虽然 Clojure 的 `cons` 也可以用相同的方式构造列表,但 *cons* 对却不存在。`cons` 的第二个函数只能是序列或者 `nil`。
```clojure
(cons 1 2)
@@ -101,7 +169,7 @@ Clojure 是门动态类型语言,一般不需要考虑太多类型问题,不
;; => (1 2 3)
```
-Clojure 的列表叫作持久化列表(PersistentList),Clojure 的所有集合数据类型几乎都是持久化(Persistent)且不可变(Immutable)的。
+Clojure 的列表叫作持久化列表(PersistentList),Clojure 的所有集合数据类型都是持久化(Persistent)且不可变(Immutable)的。
```clojure
(type '(1))
@@ -134,7 +202,7 @@ public void add(int index, Object element){
嗯?直接抛出异常了?
-令人费解,而且有违反里氏替换原则的嫌疑。仔细想想,Clojure 的确没有给列表直接添加元素的方法,我们用的是 `cons` 和 `conj`(后者与 `cons` 相反,是在序列的尾部添加元素)。那 `cons` 是怎么实现的?
+令人费解,而且违反了里氏替换原则。仔细想想,Clojure 的确没有给列表直接添加元素的方法,因为列表不可变,我们用的是 `cons` 和 `conj`(后者与 `cons` 相反,是在序列的尾部添加元素)。那 `cons` 是怎么实现的?
```java
public Cons(Object first, ISeq _more){
@@ -143,7 +211,7 @@ public Cons(Object first, ISeq _more){
}
```
-这下就清晰了,求值 `(cons 1 '(2 3))` 得到 `(1 2 3)`,原来的 `1` 和 `(2 3)` 都留在原地,我们只是创建了一个 `Cons`,指向那两个值,而 `Cons` 本身也是个序列,可以再用它创建新的 `Cons` 或者其他序列(`Cons` 是 `ASeq` 的子类,也就实现了 `ISeq`,序列接口,`PersistentList` 也是序列)。
+这下就清晰了,求值 `(cons 1 '(2 3))` 得到 `(1 2 3)`,原来的 `(2 3)` 留在原地,我们只是创建了一个 `Cons` 存放 `1`,指向 `(2 3)` 这个序列,而 `Cons` 本身也是个序列,可以再用它创建新的 `Cons`,新的 `Cons` 也会就会指向它。
这种构造持久化列表的方式叫作结构共享(Structural Sharing),既做到了不可变,又没有在内存中管理大量的数据副本。本质上,一切都是指针。
@@ -161,29 +229,6 @@ public Cons(Object first, ISeq _more){
[1 "I'm a vector" :vector true]
```
-列表和向量都可以用 `first`、`rest` 和 `cons` 函数。
-
-```clojure
-(first [1 2 3 4]) ;; => 1
-(first '(1 2 3 4)) ;; => 1
-
-(rest [1 2 3 4]) ;; => (2 3 4)
-(rest '(1 2 3 4)) ;; => (2 3 4)
-
-(cons 1 [2 3 4]) ;; => (1 2 3 4)
-(cons 1 '(2 3 4)) ;; => (1 2 3 4)
-
-;; 当然还支持其他操作序列的函数
-;; 列表和向量都可以通用
-```
-
-你可能发现执行了序列操作之后,向量的方括号变成了圆括号。不必担心,它实际上还是向量,行为是一致的。
-
-```clojure
-(type (rest [1 2 3 4]))
-;; => clojure.lang.PersistentVector$ChunkedSeq
-```
-
向量和列表最大的不同点在于,可以用索引访问向量的元素,但列表没有索引。
```clojure
@@ -209,26 +254,36 @@ public Cons(Object first, ISeq _more){
```clojure
(if (seq [1 2])
- "I am a seq"
- "I am not a seq")
-;; => "I am a seq"
+ "I can be a seq"
+ "I can not be a seq")
+;; => "I can be a seq"
```
-`seq` 的作用是把可以转换为序列的数据结构转换为序列,字符串也可以转换成序列。
+`seq` 的作用是把可以转换为序列的数据结构转换为序列,集合只要不是空的,都会返回一个正常的序列,会被条件判断当作真值。因此 `seq` 经常被用来检查一个集合是否**不为空**;相反,`emtpy?` 用来检查一个集合是否**为空**。
+
+要注意的是,如果对向量做了序列操作,比如用 `rest` 函数获取向量的第一个元素之后的所有元素,就会得到一个序列,向量的索引就不可用了:
```clojure
-(seq "string")
-;; => (\s \t \r \i \n \g)
-;; \ 用来表示 Clojure 中的字符
+(rest [1 2 3])
+;; => (2 3)
+(get (rest [1 2 3]) 1)
+;; => nil
```
-集合只要不是空的,都会返回一个正常的序列,会被条件判断当作真值。因此 `seq` 经常被用来检查一个集合是否不为空;相反,`emtpy?` 用来检查一个集合是否为空。
+可以用 `vec` 把序列变成向量
+
+```clojure
+(get (vec (rest [1 2 3])) 1)
+;; => 3
+```
-序列(sequence)是只支持线性访问,不支持随机访问的集合,可以当作列表理解。序列的行为和列表几乎是一致的,它也没有索引。
+可以用 `vector?` 检查数据是不是向量。
```clojure
-(get (seq [1 2]) 1)
-;; => nil
+(vector? [1 2 3])
+;; => true
+(vector? (seq [1 2 3]))
+;; => false
```
最后,向量其实还继承了 `AFn` 类,可以当成函数使用。
@@ -243,7 +298,7 @@ public Cons(Object first, ISeq _more){
## 花括号和映射
-Clojure 的映射写起来和 JSON 很像。
+Clojure 的映射(map)写起来和 JSON 很像。
```clojure
{:key1 "val1"
@@ -252,8 +307,430 @@ Clojure 的映射写起来和 JSON 很像。
:key3 nil}
```
-https://www.brainonfire.net/files/seqs-and-colls/main.html
+映射和向量也很像,映射不过是可以自定义索引的向量。向量只能用从 `0` 开始自增的整数索引,但映射可以自定义键。键一般是关键词类型,但也可以是别的类型,比如这是 HTTP 响应头的映射表示:
+
+```clojure
+{"Content-Type" "text/html; charset=utf-8"
+ "Cache-Control" "no-cache"
+ "Date" "Sat, 26 Sep 2026 07:06:35 GMT"}
+```
+
+一般而言,推荐使用关键词表示键,一方面是看起来更清晰,另一方面是,关键词和向量一样,可以当作函数使用,从映射里取出值:
+
+```clojure
+(:key1 {:key1 "val1"})
+;; => "val1"
+
+;; 等价于
+(get {:key1 "val1"} :key1)
+;; => "val1"
+```
+
+其他类型却不行:
+
+```clojure
+(let [data {"Content-Type" "text/html; charset=utf-8"
+ "Cache-Control" "no-cache"
+ "Date" "Sat, 26 Sep 2026 07:06:35 GMT"}]
+ ("Content-Type" data))
+;; Execution error (ClassCastException) at user/eval194 (REPL:4).
+;; class java.lang.String cannot be cast to class clojure.lang.IFn (java.lang.String is in module java.base of loader 'bootstrap'; clojure.lang.IFn is in unnamed module of loader 'app')
+
+;; 只能写作:
+(get data "Content-Type")
+```
+
+可以引入 `clojure.walk` 标准库,用这个库里的 `keywordize-keys` 方法,把不是关键词类型的键全部转换为关键词。
+
+```clojure
+(require '[clojure.walk :as walk])
+(walk/keywordize-keys {"Content-Type" "text/html; charset=utf-8"
+ "Cache-Control" "no-cache"
+ "Date" "Sat, 26 Sep 2026 07:06:35 GMT"})
+;;=> {:Content-Type "text/html; charset=utf-8",
+;; :Cache-Control "no-cache",
+;; :Date "Sat, 26 Sep 2026 07:06:35 GMT"}
+```
+
+其实把关键词和映射反过来也能达到一样的效果,因为映射和向量一样,也继承了 `AFn` 类,可以当作函数用。
+
+```clojure
+({:key1 1 :key2 2} :key2)
+;; => 2
+(:key2 {:key1 1 :key2 2})
+;; => 2
+```
+
+由于映射可以嵌套,你可能会遇到需要获取一连串键值对的情况,这时候就用 `get-in`。比方说,我们可能要处理结构非常*反人类*的 [microformats](https://microformats.org/)。
+
+```clojure
+{:webmention {:source "https://alice.blog"
+ :target "https://bob.net"
+ :microformats [{:type ["h-card"]
+ :properties {:photo [{:alt "Photo of Alice"
+ :value "https://alice.blog/photo.avif"}]
+ :name "alice"
+ :url "https://alice.blog"}}]}}
+```
+
+```clojure
+(get-in data [:webmention :microformats 0 :properties :photo 0 :value])
+;; => "https://alice.blog/photo.avif"
+```
+
+和其他集合类型一样,映射不需要写逗号分隔元素,但写了也不会报错。如果你觉得这样更清晰,可以在每个键值对后面写上逗号。==不过无论有没有逗号,奇数个元素都是键,偶数个元素都是值==。如果映射里的元素没有成对,就会报错。
+
+```clojure
+{:key nil :key2}
+;; Syntax error reading source at (REPL:51:17).
+;; Map literal must contain an even number of forms
+```
+
+键也不能重复:
+
+```clojure
+{:key nil :key nil}
+;; Syntax error reading source at (REPL:52:20).
+;; Duplicate key: :key
+```
+
+试图用 `cons` 或 `conj` 把一个键值对放进映射里,就算键重复也不会报错,因为 `cons` 返回的是个序列,并非映射。
+
+```clojure
+(cons [:key1 nil] {:key1 nil})
+;; => ([:key1 nil] [:key1 nil])
+```
+
+要往映射里添加键值对,或者覆盖某个键的值,要用 `assoc`(*associate*)。
+
+```clojure
+(assoc {:key1 nil} :key2 nil)
+;; => {:key1 nil, :key2 nil}
+
+(assoc {:key1 nil} :key1 1)
+;; => {:key1 1}
+```
+
+如果要移除某个映射,就用 `dissoc`(*dissociate*)。
+
+```clojure
+(dissoc {:key1 nil} :key1)
+;; => {}
+```
+
+## 井号和集
+
+在 `{}` 前面加上 `#`,就会得到集(set)。*Set* 其实应该叫集合,但我担心和 *Collections* 混淆,所以称作「集」。顾名思义,集就是不能有重复数据的集合。
+
+```clojure
+#{1 1 2 3}
+;; Syntax error reading source at (REPL:61:11).
+;; Duplicate key: 1
+```
+
+注意看,这里的报错是 `Duplicate key`(重复的键)。实际上,集就是把键和值合二为一的映射,不妨试试用关键词作为函数,取出集合里的关键词。
+
+```clojure
+(:key1 #{:key1 :key2 :key3})
+;; => :key1
+```
+
+向量和列表都不能这么干,因为向量的键是整数,而列表没有键。
+
+```clojure
+(:key1 [:key1 :key2])
+;; => nil
+(:key1 '(:key1 :key2))
+;; => nil
+```
+
+向量和映射都可以作为函数,集合也可以。
+
+```clojure
+(#{1 2 3} 2)
+;; => 2
+(#{1 2 3} 100)
+;; => nil
+```
+
+如果参数存在于集合中,就会返回集合中的这个元素;如果不存在,就会返回 `nil`。由于 `nil` 是假值,所以可以用这种方式判断某个元素是否存在于集合中。
+
+```clojure
+(let [allowed-domains #{"kagi.com" "duckduckgo.com"}]
+ (if (allowed-domains "google.com")
+ "Domain allowed"
+ "Domain not allowed"))
+;; => "Domain not allowed"
+```
+
+## 检查集合
+
+我们已经知道,可以用 `seq` 检查集合是否非空,用 `empty?` 检查集合是否为空;可以用 `seq?`、`vector?`、`list?`、`sequential?`、`map?` 和 `set?` 检查数据是否为序列、向量、列表、连续的、映射或集合。
+
+除了对集合数据本身做检查,我们可能还想要对集合里面的元素做检查。
+
+用 `every?` 检查集合里的**每个元素**是否都满足条件。
+
+```clojure
+(every? even? [2 4 6])
+;; => true
+(every? odd? [1 2 4])
+;; => false
+```
+
+`every?` 接收两个参数,第一个参数 `pred` 是直接接收一个变量的谓词函数,第二个参数 `coll` 是要检查的集合。这个函数对每个元素求值一次 `(pred element)`,只要有一个是 `false`,就返回 `false`,否则返回 `true`。
+
+`even?` 和 `odd?` 分别检查数字是否为奇数和是否为偶数。
+
+第二个参数可以是匿名函数 `fn`:
+
+```clojure
+(every? (fn [x] (= x 1)) [1 1 1])
+;; => true
+```
+
+匿名函数可以简写为 `#()`,用 `%` 表示参数:
+
+```clojure
+(every? #(= % 1) [1 1 1])
+;; => true
+```
+
+如果不要求所有元素,只要求某一些元素符合要求,就用 `some`。
+
+```clojure
+(some #(= % 1) [1 2 3])
+;; => true
+(some #(= % 1) [2 3 4])
+;; => false
+```
+
+为什么 `some` 没有 `?` 呢?那是因为 `some?` 是另一个函数,用来判断某个值是否**不是** `nil`。
+
+```clojure
+(some? 0)
+;; => true
+(some? [1 2 3])
+;; => true
+(some? nil)
+;; => false
+```
+
+用来判断某个值**是** `nil` 的函数是 `nil?`,其实 `some?` 就是 `(not (nil? x))`,[源代码](https://github.com/clojure/clojure/blob/clojure-1.12.4/src/clj/clojure/core.clj#L533)就是这么写的。
+
+有点绕,对吧?还有更绕的。
+
+Clojure 新手常见的问题是搞不清楚 `contains?` 的作用。一般人会下意识地以为这个函数用来检查集合是否*包含*某个元素,但很快发现并非如此。
+
+```clojure
+(contains? [5 7 8] 5)
+;; => false
+(contains? [5 7 8] 0)
+;; => true
+```
+
+`contains?` 用于检查集合中是否包含某个键(key)。我们知道向量就是键为自增整数的映射,`[5 7 8]` 中各个元素的键或者说索引分别是 `0 1 2`,所以用 `contains?` 查找 `5` 会返回 `false`,但查找 `0` 会返回 `true`。
+
+`contains?` 可以用来测试映射中是否包含某个键,集是否包含某个元素:
+
+```clojure
+(contains? {:key1 "value"} :key1)
+;; => true
+(contains? #{1 4 2} 4)
+;; => true
+```
+
+如果要检查列表和向量中是否包含某个值(而非键),应该用什么呢?其实你已经会了。仔细想想,用上面提到过的哪些函数可以做到这点?
+
+<details>
+ <summary>答案揭晓</summary>
+
+如果你的答案是:
+
+```clojure
+(some #(= % :want) [:some :element :here])
+;; => false
+(some #(= % :want) [:i :want :to :learn :clojure])
+;; => true
+```
+
+恭喜你对了一半!更 *Idiomatic* 的写法是:
+
+```clojure
+(some #{:want} [:some :element :here])
+;; => nil
+(some #{:want} [:i :want :to :learn :clojure])
+;; => :want
+```
+
+还记得吗?集也可以当作函数用。
+
+</details>
+
+## 操作集合
+
+如果你第一时间想到的是用 `for` 循环遍历和操作集合,~~那你可得好好反思一下~~。别担心,我刚开始也一直试图弄懂怎么在 Clojure 里写循环结构(也就是 `for`、`while`、`do ... while`),后来发现,我根本不需要。至少在我目前写 Clojure 的一百多个小时里,我一次也没有用过 `for` 和 `while`。
+
+### `filter` 和 `remove`
+
+假设我们有这样一个列表。
+
+```clojure
+(def numbers
+ '(1 43 231 64 23 99))
+```
+
+我希望删除里面的偶数,我该怎么办?这样吗?
+
+```clojure
+;; (take n coll) 返回由 coll 前 n 个元素组成的序列
+;; (drop n coll) 返回丢弃 coll 前 n 个元素后的序列
+;; (concat coll coll) 把两个集合拼接起来
+;; (inc n) 相当于 (+ 1 n)
+(for [n numbers]
+ (if (even? n)
+ (let [i (.indexOf numbers n)]
+ (concat (take i numbers) (drop (inc i) numbers)))
+ numbers))
+;; => ((1 43 231 64 23 99)
+;; (1 43 231 64 23 99)
+;; (1 43 231 64 23 99)
+;; (1 43 231 23 99)
+;; (1 43 231 64 23 99)
+;; (1 43 231 64 23 99))
+```
+
+*欸?不对,Clojure 数据是不可变的,我只是返回了很多个新的列表,其中有一些是去除了一个偶数的。照理来说,我应该声明一个空列表,然后把不是偶数的元素放进去…… 欸?Clojure 的赋值语句是什么?嗯?Clojure 是函数式语言,那是什么意思?啊?没有赋值语句,绑定了符号就不能改了?*
+
+我的朋友,你的问题,只需要一行代码解决:
+
+```clojure
+(remove even? numbers)
+;; => (1 43 231 23 99)
+```
+
+或者这样:
+
+```clojure
+(filter odd? numbers)
+;; => (1 43 231 23 99)
+```
+
+和刚才的 `every?` 和 `some` 一样,`remove` 和 `filter` 接收一个谓词函数和一个集合,分别用来移除符合条件的元素,或保留符合条件的元素。
+
+### `reduce`
+
+现在我又有新的需求了,我要把这些数字累加起来,应该怎么做?
+
+```clojure
+(reduce + numbers)
+;; => 461
+```
+
+`reduce` 接收函数 `f` 和集合 `coll`,其中 `f` 应该要能接收两个参数。它先对前两个元素求值 `(f x y)`,然后对下一个元素求值 `(f (f x y) z)`,以此类推。这里我们用的是 `+`,所以就实现了累加。
+
+还可以传入初始值,这样 `reduce` 就不会从 `(f x y)` 开始求值了。我们能用它来拼接集合。
+
+```clojure
+(reduce conj [1 2] [3 4])
+;; => [1 2 3 4]
+
+;; 直接用 conj 的话,第二个参数会被当作一整个元素放进去
+(conj [1 2] [3 4])
+;; => [1 2 [3 4]]
+```
+
+`(reduce conj a b)` 的简便写法是 `into`:
+
+```clojure
+(into [1 2] [3 4])
+;; => [1 2 3 4]
+
+;; concat 也能拼接集合,但返回的是序列
+(concat [1 2] [3 4])
+;; => (1 2 3 4)
+
+;; 操作向量时,最好用 into
+```
+
+练习一下,如果要把下面这个向量打平(flatten)然后去重,应该怎么做?
+
+```clojure
+(def flatten-me
+ [[1 2] [53 13 5] [2] [12 53 55]])
+```
+
+<details>
+ <summary>答案揭晓</summary>
+
+```clojure
+(reduce into [] flatten-me)
+;; => [1 2 53 13 5 2 12 53 55]
+```
+
+去重只需要把向量变成集合,然后再变回来。
+
+```clojure
+(vec (set (reduce into [] flatten-me)))
+;; => [1 55 13 12 2 5 53]
+```
+
+</details>
+
+### `map`
+
+如果我想要把列表里的数字全部翻倍,应该怎么做?
+
+`map` 函数接收一个函数 `f` 和一个集合 `coll`,把 `f` 作用在每个元素上之后,返回新的集合。
+
+```clojure
+(map #(* % 2) numbers)
+;; => (2 86 462 128 46 198)
+```
+
+要注意的是,`map` 返回的是个序列(准确来说是惰性序列,后面再讲)。如果想要返回向量,就换成 `mapv`。
+
+```clojure
+(map #(* % 2) (vec numbers))
+;; => (2 86 462 128 46 198)
+(mapv #(* % 2) (vec numbers))
+;; => [2 86 462 128 46 198]
+```
+
+## 箭头宏
+
+单用上面这些函数中的任何一个是无法实现复杂需求的。假设我想要把列表中的偶数删掉,然后对剩下的奇数求平方,最后过滤掉超过 `1000` 的数,再累加起来。你可能就能感受到什么是 *Lost In Stupid Parentheses* 了:
+
+```clojure
+(reduce + (filter #(<= % 1000) (map #(* % %) (remove even? numbers))))
+;; => 530
+```
+
+可以发现这些函数全都是类似的形式:`(func f coll)`。函数放在中间,集合放在最后面。有一个宏,可以自动把他的参数嵌套在一起,把上一个参数放进下一个参数的末尾。
+
+```clojure
+(->> numbers
+ (remove even?)
+ (map #(* % %))
+ (filter #(<= % 1000))
+ (reduce +))
+;; => 530
+```
+
+`->>` 叫作 *thread-last*,似乎也有翻译成「线程宏」的,但 *thread* 只是把层层嵌套的函数调用比喻成一条线而已,或许可以叫「线宏」,但我更喜欢叫它「箭头宏」。
+
+*thread-last* 是说,把上一个函数形式放进下一个函数形式的最后一个参数的位置。自然还有 *thread-first*,写作 `->`。如果要传递和操作的数据都在第一个参数的位置,或者这条线上的函数都只接收一个参数,就可以用 `->`。
+
+学会使用箭头宏,在 Clojure 里你经常需要把数据在函数之间传来传去,它能帮你少写很多括号,而且代码会更简单易读。
+
+---
+
+## 最后
+
+我已经写了五千多字,还没写到惰性。我也犯懒了,读者如果着急,可以先阅读我今年年初写的《[计算无穷](/article/compute-infinity/)》来了解惰性是什么。
+
+另外,最近有不少读者表达了对这个系列的喜爱,很高兴看到有人对 Clojure 感兴趣!希望我突然写这么长一篇文章没有把你们吓到。没掌握也没关系,可以随时会来查阅。之后需要用到集合的地方很多,有很多机会练习。
-## 井号和集合
+回见!
[^1]: 可参考:《[Just A Common Lisper](/article/common-lisp-start/)》