# 转换器
转换器(transducer)是一种可复用的转换管道,独立于数据源或汇点。转换器让你只需编写一次“map + filter + take”部分,就能将其应用于任何集合、归约函数或通道。
转换器是有状态函数:`(rfn) → rfn'`。它们可以组合、提前终止,并且不会分配中间集合。
## 快速开始
# A transducer is built by composing 1-arg forms def xf [comp [map {|x| ($x * 2)}] [filter is-even] [take 10]] # Apply it wherever a reducing context is expected [into @{} xf $xs] # conj into a list [into ^{} xf $xs] # add into a set [transduce xf + 0 $xs] # fold via a plain fn [transduce xf [completing +] 0 $xs] # explicit Reducer wrap
## 两种形式
`map`、`filter`、`take`、`drop` 和 `mapcat` 是按参数个数重载的:
- 单参数返回转换器:`[map f]`、`[filter pred?]`、`[take n]`
- 双参数返回惰性序列:`[map $xs f]`、`[filter $xs pred?]`
惰性序列形式在惰性序列中介绍。转换器是用 `comp` 组合的单参数形式。
## 组合转换器
`comp` 从左到右组合转换器(数据按声明顺序流经它们):
def xf [comp [filter { ($it % 2) == 0 }] [map { $it + 1 }] [take 3]] [into @{} xf @{ 1 2 3 4 5 6 7 8 }] # => @{ 3 5 7 }
注意,`comp` 按源顺序执行 —— `filter` 在 `map` 之前、`map` 在 `take` 之前 —— 这与数学上的函数复合不同。
## 应用转换器
### `into` — 将元素 conj 进集合
`[into target xf source]` 让 `source` 流经 `xf`,将每个结果元素 conj 到 `target` 中。target 可以是列表字面量、集合字面量、映射字面量,或任何现有集合:
[into @{} xf $xs] # list
[into ^{} xf $xs] # set (duplicates collapse)
### `transduce` — 使用自定义归约函数折叠
[transduce xf + 0 $xs] # sum of transformed elements [transduce xf str "" $xs] # concatenate
归约函数可以是普通的双参数可调用对象(通过 `completing` 自动包装),也可以是 `%Reducer{:init :step :done}` 标签映射。
### `%Reducer` — 完整的归约协议
归约函数是 `%Reducer{:init :step :done}` 标签映射。`:init` 和 `:done` 键可以为 `nil`。`completing` 将普通的双参数函数包装为 `%Reducer`:
def sum-reducer %Reducer{ :init {|| 0} :step {|acc x| ($acc + $x)} :done {|result| [println "sum was $result"] } } [transduce xf $sum-reducer 0 $xs]
## 提前终止
`(take n)` 会在任何归约上下文中短路 —— 不仅是惰性序列。底层使用 `%Reduced`:
# A transducer chain with take inside def xf [comp [map { $it * 2 }] [take 3]] # Even when transducing into a sum, the chain stops after 3 elements [transduce xf + 0 @{ 1 2 3 4 5 6 7 8 9 10 }] # => 12 (only 1,2,3 were processed: 2 + 4 + 6)
### `reduced` / `reduced?` / `unreduced`
构建你自己的提前终止步骤:
def first-even {| set step {|acc x| if (is-even x) { [reduced x] # wrap to terminate } { $acc } | } %Reducer{ :init {|| nil} :step $step :done nil } | } [transduce [filter identity] $first-even nil @{ 1 3 5 6 7 }] # => 6 [reduced? [reduced 42]] # => true [unreduced [reduced 42]] # => 42 [unreduced 42] # => 42 (passthrough)
## 惰性序列 vs 转换器
| 方面 | 惰性序列 | 转换器 |
|---|---|---|
| 数据源 | 始终是集合 | 任何归约上下文 |
| 汇点 | 始终是列表(通过 `collect`) | 列表、集合、映射、求和、自定义 |
| 复用 | 每次调用 | 可跨集合复用 |
| 提前终止 | 链中的 `take n` | 任意步骤中的 `take n` 或 `%Reduced` |
| 最适合 | 一次性、输出列表、可读性佳 | 可复用、多汇点、性能关键 |
## 何时使用转换器
- 同一条转换管道要应用于多个集合或汇点。
- 汇点不是向量(集合、映射、求和、自定义归约函数)。
- 你希望整条链上显式提前终止。
- 你要向通过归约函数消费的通道或异步任务提供数据。
对于一次性、集合到集合的转换,若可读性最重要,优先使用 `|>` 管道(参见惰性序列)。
## 示例
### 前 10 个偶数的平方和
def xf [comp [filter { ($it % 2) == 0 }] [map { $it * $it }] [take 10]] [transduce xf + 0 [range 0 1000]] # => 0 + 4 + 16 + ... + 36 (= sum of squares of 0,2,4,...,18)
### 将列表索引为映射
def index-by-name [comp [map {|x| %{ :key $x~name :val $x }}] (mapcat {|kv| @{ $kv }})]) # unfold into key/val pairs [into ^{} index-by-name $users] # set of users by name
### 与多个数据源组合
def normalize [comp [map trim] [filter not-empty?] [map lower]] [into @{} normalize $raw-lines] # one source [into @{} normalize $other-lines] # different source, same pipeline
## 快速参考
| 形式 | 用途 |
|---|---|
| `[map f]` | 单参数转换器 |
| `[filter pred?]` | 单参数转换器 |
| `[take n]` / `[drop n]` | 单参数转换器 |
| `[mapcat f]` | 单参数转换器 |
| `[comp xf1 xf2 ...]` | 组合转换器 |
| `[into target xf source]` | conj 到集合中 |
| `[transduce xf rfn init source]` | 通过归约函数折叠 |
| `[completing f]` | 将普通函数包装为 `%Reducer` |
| `[reduced x]` / `[reduced? x]` / `[unreduced x]` | 提前终止协议 |