文章目录

Clojure 序列操作:map、filter、reduce

发布于 2026-04-10 20:18:26 · 浏览 9 次 · 评论 0 条

Clojure 序列操作:map、filter、reduce

Clojure 处理数据的核心在于对序列的操作。大多数编程任务最终都可以归纳为:转换数据、筛选数据和汇总数据。这三个动作在 Clojure 中分别对应 mapfilterreduce 三个核心函数。


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])

执行步骤如下:

  1. 初始值 0 和第一个元素 1执行 (+ 0 1),得到 1
  2. 上一步结果 1 和第二个元素 2执行 (+ 1 2),得到 3
  3. 上一步结果 3 和第三个元素 3执行 (+ 3 3),得到 6
  4. 上一步结果 6 和第四个元素 4执行 (+ 6 4),得到 10

最终结果为 10

为了更直观地理解数据流动过程,可以参考下面的流程图:

graph LR subgraph Sequence["输入序列 [1, 2, 3, 4]"] S1[1] S2[2] S3[3] S4[4] end Start((开始: acc=0)) --> Op1["运算: 0 + 1"] Op1 --> R1["结果: 1"] S1 --> Op1 R1 --> Op2["运算: 1 + 2"] Op2 --> R2["结果: 3"] S2 --> Op2 R2 --> Op3["运算: 3 + 3"] Op3 --> R3["结果: 6"] S3 --> Op3 R3 --> Op4["运算: 6 + 4"] Op4 --> End((结束: result=10)) S4 --> Op4

构建复杂数据

reduce 不仅仅可以计算数字,还可以构建数据结构。例如,我们将一个向量转换成 Map。

输入以下代码:

(reduce conj {} [[:a 1] [:b 2]])

这里 conj 是向集合中添加元素的函数。初始值是一个空 Map {}

  1. 执行 conj {} [:a 1],得到 {:a 1}
  2. 执行 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 个值 求和、统计、合并数据

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文