内容提要
本文介绍Clojure的集合数据类型:普通类型包括数字、双精度浮点数、字符串、布尔值和关键词;集合分为列表、向量、映射和集,均为持久化不可变,并通过结构共享高效复用内存。列表是序列,向量支持索引,映射以键取值,集不含重复元素。文中还讲解了seq、filter、remove、reduce、map等操作及箭头宏->>的用法。
延伸解读
持久化与结构共享:不可变集合的内存效率
Clojure 的集合默认不可变,修改操作返回新集合,原集合保持不变。这得益于结构共享:新集合复用旧集合的大部分节点,仅创建必要的差异部分。例如,向列表头部添加元素时,cons 创建一个新节点指向原列表,而非复制整个列表。因此,虽然数据不可变,但内存占用和操作效率都很高。这种设计让 Clojure 在函数式语言中性能表现突出,也避免了并发修改的风险。
序列抽象:统一操作不同集合的基石
序列(seq)是 Clojure 的核心抽象,它只要求支持 first、next 等线性访问操作。列表本身就是序列,而向量、映射、集甚至字符串都可以通过 seq 函数转换为序列。许多集合操作(如 map、filter、reduce)都基于序列抽象,因此能通用于各种集合。但要注意,对向量使用 rest 等序列操作后,结果不再是向量,索引访问会失效,必要时可用 vec 转回向量。
contains? 的陷阱:检查键而非值
Clojure 的 contains? 函数容易误解:它检查的是集合中是否存在某个键(key),而非值。对于向量,键是索引,所以 (contains? [5 7 8] 0) 返回 true,而 (contains? [5 7 8] 5) 返回 false。对于映射和集,它分别检查键和元素。若要检查列表或向量中是否包含某个值,应使用 some 配合集或谓词函数,例如 (some #{:want} coll),这样更符合 Clojure 的惯用风格。
箭头宏 ->>:简化嵌套函数调用的利器
当需要串联多个集合操作时,嵌套调用会难以阅读。箭头宏 ->>(thread-last)将前一个表达式的结果作为下一个函数的最后一个参数,从而把嵌套结构改为线性序列。例如,先移除偶数、再平方、过滤大于1000的数、最后求和,用 ->> 写出来一目了然。如果数据总是作为第一个参数传递,则可用 ->(thread-first)。掌握箭头宏能显著减少括号并提升代码可读性。
Q&A
Clojure 的集合数据类型有哪些?它们都是不可变的吗?
Clojure 的集合数据类型包括列表、向量、映射和集。它们都是持久化且不可变的,对数据的操作只会产生新的数据,而不会修改原来的数据。
Clojure 中列表和向量有什么区别?
列表是序列,只支持线性访问,没有索引;向量支持索引,可以通过索引随机访问元素。列表用圆括号表示,向量用方括号表示。只有列表是序列,向量不是序列,但向量实现了 Sequential 接口。
Clojure 的映射和集有什么特点?如何使用?
映射以键值对形式存储数据,键通常是关键词,可以用作函数从映射中取值。集是不包含重复元素的集合,本质上键和值合二为一的映射,也可以作为函数检查元素是否存在。
Clojure 中 filter、remove、reduce 和 map 这些函数分别有什么作用?
filter 保留满足条件的元素,remove 移除满足条件的元素,reduce 对集合元素进行累积操作,map 对每个元素应用函数并返回新的序列。它们都接收一个函数和一个集合。
Clojure 的箭头宏 ->> 是什么?有什么作用?
->> 是 thread-last 宏,它将上一个函数形式插入到下一个函数形式的最后一个参数位置,从而将嵌套的函数调用线性化,减少括号嵌套,使代码更易读。
Clojure 中的结构共享是什么?它有什么好处?
结构共享是 Clojure 实现持久化数据结构的一种方式,新数据通过共享原有数据的部分内存来构造,避免大量数据副本,从而高效利用内存。例如,cons 操作创建一个新的 Cons 节点指向原有序列。