Clojure 序列操作:map、filter、reduce
Clojure 处理数据的核心在于对序列的操作。大多数编程任务最终都可以归纳为:转换数据、筛选数据和汇总数据。这三个动作在 Clojure 中分别对应 map、filter 和 reduce 三个核心函数。
1. 数据转换:map
map 的作用是将一个函数应用到序列的每一个元素上,返回一个包含新结果的新序列。这是一种“一对一”的转换。
理解基本用法
map 接收两个参数:一个函数和一个集合。
打开你的 REPL 或编辑器,输入以下代码:
(map inc [1 2 3 4])
运行后,你会得到结果 (2 3 4 5)。
这里 inc 是 Clojure 内置的加一函数,map 遍历了列表中的每个数字并分别对其加一。
处理复杂数据结构
在实际开发中,我们经常需要处理包含 Map 的向量(类似 JSON 数组)。假设你有一组用户数据,需要提取所有人的名字。
定义一个用户向量:
(def users [{:name "Alice" :age 25}
{:name "Bob" :age 30}
{:name "Charlie" :age 35}])
使用 map 配合 :name 关键字来提取名字。在 Clojure 中,关键字可以作为函数使用,用于从 Map 中获取对应的值。
输入以下代码:
(map :name users)
观察输出结果 ("Alice" "Bob" "Charlie")。
使用匿名函数
当内置函数无法满足需求时,编写一个匿名函数传给 map。
输入以下代码,将年龄增加 1 岁:
(map (fn [user] (update user :age inc)) users)
这里 使用了 update 函数,它接收 Map、键和一个更新函数,返回更新后的 Map。
2. 数据筛选:filter
filter 用于根据条件从集合中保留或剔除元素。它接收一个“断言函数”(返回 true 或 false 的函数)和一个集合。
筛选数字
假设你有一个数字列表,只想保留其中的偶数。
输入以下代码:
(filter even? [1 2 3 4 5 6])
运行结果为 (2 4 6)。
筛选复杂结构
继续使用上面的 users 数据,假设我们需要找出年龄大于 30 岁的用户。
编写筛选条件:
(filter (fn [user] (> (:age user) 30)) users)
或者 使用更简洁的匿名函数写法 #(...):
(filter #(< 30 (:age %)) users)
这里 % 代表传入的参数(即单个 user Map)。
取反操作
如果你想要剔除满足条件的元素,可以 使用 remove 函数,它的用法与 filter 完全一致,但逻辑相反。
3. 数据汇总:reduce
reduce 是这三个函数中最强大的。它将一个序列“折叠”或“归纳”成一个单一值。它通常用于求和、求积或构建复杂的数据结构。
理解工作机制
reduce 接收三个参数:一个函数、一个初始值(可选)和一个序列。
它的核心逻辑是:函数的上一次计算结果(累加器)会和序列的下一个元素一起,再次传入函数进行计算。
我们可以用数学公式来表达这个过程。假设函数为 $f$,序列为 $[x_1, x_2, x_3]$,初始值为 $acc_0$,那么计算过程如下:
$$
acc_1 = f(acc_0, x_1)
$$
$$
acc_2 = f(acc_1, x_2)
$$
$$
result = f(acc_2, x_3)
$$
求和示例
计算数字列表的总和。
输入以下代码:
(reduce + 0 [1 2 3 4])
执行步骤如下:
- 取初始值
0和第一个元素1,执行(+ 0 1),得到1。 - 取上一步结果
1和第二个元素2,执行(+ 1 2),得到3。 - 取上一步结果
3和第三个元素3,执行(+ 3 3),得到6。 - 取上一步结果
6和第四个元素4,执行(+ 6 4),得到10。
最终结果为 10。
为了更直观地理解数据流动过程,可以参考下面的流程图:
构建复杂数据
reduce 不仅仅可以计算数字,还可以构建数据结构。例如,我们将一个向量转换成 Map。
输入以下代码:
(reduce conj {} [[:a 1] [:b 2]])
这里 conj 是向集合中添加元素的函数。初始值是一个空 Map {}。
- 执行
conj {} [:a 1],得到{:a 1}。 - 执行
conj {:a 1} [:b 2],得到{:a 1 :b 2}。
4. 实战组合:Thread-Last Macro
在实际开发中,我们经常需要将这三个函数串联起来使用。例如:“从用户列表中过滤出成年人,提取他们的名字,然后将名字合并成一个字符串”。
如果不使用宏,代码会变成这样:
(apply str (interpose ", " (map :name (filter #(< 18 (:age %)) users))))
这种嵌套结构极难阅读。Clojure 提供了 ->>(Thread-Last Macro)来解决这个问题。它将上一步的结果作为最后一个参数传递给下一个函数。
使用 ->> 重写上述逻辑:
(->> users
(filter #(< 18 (:age %))) ; 1. 筛选年龄大于18的
(map :name) ; 2. 提取名字
(interpose ", ") ; 3. 在名字中间插入逗号
(apply str)) ; 4. 将所有字符串拼接起来
阅读代码的顺序现在变成了从上到下,完全符合数据处理流水线的思维逻辑。
为了方便记忆和区分,下表总结了这三个函数的核心特征:
| 函数名 | 核心用途 | 输入数量 | 输出数量 | 典型场景 |
|---|---|---|---|---|
map |
转换 | N 个元素 | N 个元素 | 数据格式化、提取字段 |
filter |
筛选 | N 个元素 | 0 到 N 个元素 | 查找符合条件的数据 |
reduce |
汇总 | N 个元素 | 1 个值 | 求和、统计、合并数据 |

暂无评论,快来抢沙发吧!