内容提要
本文介绍Clojure中的归约函数与Transducer。归约函数形如(f result input),可嵌套,reduce默认左折叠。Transducer接收归约函数并返回新归约函数,如(map inc)与+组合可在累加时顺带自增,无需生成中间序列。它将“做什么”与“怎么做”解耦,不依赖具体数据类型,可用于reduce、into、sequence和channel,并可通过comp组合复用,写出更简洁高效的代码。
延伸解读
Transducer 的核心:解耦“做什么”与“怎么做”
文章指出,传统 map、filter 等序列函数同时预设了操作内容(做什么)和执行方式(怎么做,如惰性、顺序、返回序列)。Transducer 将“做什么”抽象为独立函数,把“怎么做”交给 reduce、into、sequence 或 channel 等具体执行器。这种解耦让同一转换逻辑能用于不同数据类型和上下文,避免为适配类型而反复转换序列。
组合与复用:comp 构造可独立使用的转换流程
通过 comp 组合多个 Transducer,可以定义如“过滤奇数再乘二”的独立操作,并直接用于 reduce、into、sequence 或 channel。文章强调,comp 组合的是 Transducer 而非普通序列函数,因此结果需与归约函数或具体执行器配合使用。这种复用减少了重复代码,也让转换逻辑更清晰、更易测试。
性能与类型优势:避免中间序列
文章以 (reduce ((map inc) +) 0 [1 2 3 4]) 为例,说明 Transducer 能在累加过程中顺带完成自增,无需先生成自增后的中间序列。类似地,into 可直接将转换结果放入向量或集合,省去“序列转回原类型”的步骤。这种特性减少了中间数据结构,有助于写出更简洁高效的代码,但文章未给出具体性能数据。
理解门槛与适用边界
文章坦言 Transducer 涉及对函数的操作,内容可能略显抽象,没理解也不影响继续写 Clojure。它并非必需,而是提供更多控制权与复用手段。读者需注意区分 Transducer 与普通函数组合:comp 作用于 Transducer 时,结果需与 reduce、into 等配合,而非直接操作序列。
Q&A
Clojure 中的归约函数是什么?它有什么特点?
归约函数是形如 (f result input) 的函数,接收两个参数并返回新结果。它可以相互嵌套使用,通常传给 reduce 使用。例如 + 就是一个归约函数,可以用于累加。
Clojure 的 reduce 默认是左折叠还是右折叠?如何实现右折叠?
Clojure 的 reduce 默认是左折叠(foldl)。要实现右折叠,只能先将列表倒序,再使用 reduce,例如 (reduce f (reverse coll))。
Transducer 是什么?它如何工作?
Transducer 是一个接收归约函数并返回新归约函数的函数,形如 (t (f result input)) -> (new-f result input)。它可以将“做什么”与“怎么做”解耦,不依赖具体数据类型,可用于 reduce、into、sequence 和 channel 等。
使用 Transducer 有什么好处?能举个例子吗?
Transducer 将数据转换算法与具体数据类型解耦,便于组合复用,能写出更简洁高效的代码。例如,使用 (map inc) 作为 Transducer 与 + 组合,可以在累加过程中直接自增,无需生成中间序列:(reduce ((map inc) +) 0 [1 2 3 4]) => 14。
如何组合多个 Transducer?组合后可以用在哪些地方?
可以使用 comp 函数组合多个 Transducer,例如 (comp (filter odd?) (map #(* 2 %)))。组合后的 Transducer 可用于 reduce、into、sequence、chan 等,例如 (into #{} double-odd-numbers [1 2 3 4 5 6 7 8]) 返回 #{6 2 14 10}。
Transducer 和普通函数组合(如 comp)有什么区别?
普通函数组合如 (comp #(map (fn [x] (* 2 x)) %) #(filter odd?)) 返回的函数直接操作序列;而 Transducer 组合如 (comp (filter odd?) (map #(* 2 x))) 返回的函数需要与 reduce、into、sequence、chan 等结合使用,它组合的是 Transducer 而非序列操作函数。