从 Clojure sequence 到 transducer
Table of Contents
sequence, map, filter & reduce
Clojure 很多算法/函数基于 sequence 定义. sequence 是一个逻辑列表,其接口很简单1:
;;; (first coll)
(first '(0 1 2)) ;=> 0
;;; (rest coll)
(rest '(0 1 2)) ;=> (1 2)
;;; (cons item seq)
(cons 0 '(1 2)) ;=> '(0 1 2)
诸如 map, filter 之类的函数默认实现基于 sequence 接口,比如 map 和 filter 最天真的实现方式如下:
;;; 这个实现假设你知道
;;; 1. (seq coll) 在 coll 为空返回 nil (是逻辑 false)
;;; 2. nil 是空 sequence, 即 (cons 1 nil) -> '(1)
(defn map [f coll]
(when (seq coll)
(cons (f (first coll)) (map f (rest coll)))))
(defn filter [f coll]
(when (seq coll)
(if (f (first coll))
(cons (first coll) (filter f (rest coll)))
(recur f (rest coll)))))
类似的 reduce 可以实现成:
(defn reduce
([f coll]
(assert (seq coll))
(reduce f (first coll) (rest coll)))
([f val coll]
(if (seq coll)
(recur f (f val (first coll)) (rest coll))
val)))
为什么把 reduce 的实现单独列出来?它特殊的点在于 map 和 filter 可以基于它实现:
(defn reverse [coll]
(reduce (fn [r v] (cons v r)) nil coll))
(defn map [f coll]
(reduce (fn [r v] (cons (f v) r)) nil (reverse coll)))
(defn filter [f coll]
(reduce (fn [r v] (if (f v) (cons v r) r)) nil (reverse coll)))
因此,可以说 reduce 是某种更为基础的操作。可以看出 reduce 函数实际为 map 和 filter 这种相对“上层”的操作提供了遍历数据的接口。
既然
reduce 函数可以用于封装对于数据的遍历,我们可以直接定义一个
CollReduce
协议:
;; 取自 clojure.core.protocols
(defprotocol CollReduce
(coll-reduce [coll f] [coll f val]))
这样我们可以反转下思维模型,我们不是通过 sequence 接口实现了 reduce
函数,而是为 sequence 数据类型实现了
CollReduce
协议2。
这个说法的变化不只是文字游戏。观察 sequence 接口能看出 sequence
数据是有序的,你需要通过 (first d), (first (rest d)), (first
… (rest d)) 去依次遍历所有条目。而
CollReduce
协议并不隐含这一点,即你可以为底层数据实现更为高效的遍历逻辑(比如并行的遍历)。
Reducing 函数 & Reducer
上面定义了 CollReduce
协议之后,reduce
函数可以直接定义成:
(defn reduce
[f init coll] (clojure.core.protocols/coll-reduce coll f init))
这样,对于数据的 reduce 依赖于我们对于特定数据给出的
CollReduce
协议实现.
Reducing 函数即上面我们传给
reduce
函数的那个函数,它的形状是:
(f result input) -> new-result
对于 reducing 函数的转换函数(一般用
xf
表示)形状如下:
(xf reducing-fn) -> reducing-fn
正如实现了 sequence 接口 (first, rest, cons) 的数据是 sequence
数据一样,实现了 CollReduce
协议的数据是一个 reducible
的抽象数据类型。对于 sequence, 我们期望 map/filter
等函数返回一个逻辑的集合,对于 reducible 我们也可以对其应用 reducing
函数以得到一个新的 reducible:
(defn reducer
([coll xf]
(reify
clojure.core.protocols/CollReduce
(coll-reduce [_ f1 init]
(clojure.core.protocols/coll-reduce coll (xf f1) init)))))
;; map 基于 sequence 接口 (vector 实现了 sequence 接口) 实现, 应用于 sequence,
;; 得到一个新的 sequence
(reduce + 0 (map inc [1 2 3 4]))
;; reducer 应用于 reducible (vector 实现了 CollReduce 协议), 得到一个新的
;; reducible 数据
(reduce + 0 (reducer [1 2 3 4]
(fn [reducing-fn]
(fn [result input]
(reducing-fn result (inc input))))))
将上面这个针对 reducible 数据每个条目执行特定操作的 reducing 转换函数 (xf) 做一下抽象可以得到:
(defn mapping [f]
(fn [reducing-fn]
(fn [result input]
(reducing-fn result (f input)))))
(reduce + 0 (reducer [1 2 3 4] (mapping inc)))
;;; 针对 reducible 数据的 map 实现:
(defn rmap [f coll] (reducer coll (mapping f)))
(reduce + 0 (rmap inc [1 2 3 4]))