# 惰性序列
`map`、`filter`、`take`、`drop` 和 `mapcat` 在接收集合参数时返回惰性序列。在你消费结果之前,计算不会执行。这让针对大型或无限输入的流水线保持高效。
## 什么是惰性?
惰性序列按需计算其元素。下面的链式调用不会遍历整个输入,只计算终止操作所需的部分:
@{ 1 2 3 4 5 6 7 8 9 10 }
|> filter { ($it % 2) == 0 }
|> map { $it * 10 }
|> take 2
|> collect
# => @{ 20 40 }
# only the first two even numbers were mapped
上面的流水线一旦 `take 2` 取得两个值就立即终止。即使源是无限的,该流水线也仍能完成。
## 返回惰性序列的操作
| 操作 | 1 参数(转换器) | 2 参数(惰性序列) |
|---|---|---|
| `map` | `[map f]` | `[map $xs f]` 或 `$xs \|> map f` |
| `filter` | `[filter pred?]` | `[filter $xs pred?]` 或 `$xs \|> filter pred?` |
| `take` | `[take n]` | `[take $xs n]` 或 `$xs \|> take n` |
| `drop` | `[drop n]` | `[drop $xs n]` 或 `$xs \|> drop n` |
| `mapcat` | `[mapcat f]` | `[mapcat $xs f]` 或 `$xs \|> mapcat f` |
1 参数形式返回转换器(参见 转换器)。
## 惰性序列的求值
惰性序列不能直接打印。使用终止操作强制求值:
set xs @{ 1 2 3 4 5 } $xs |> map { $it * 2 } |> collect # force to list $xs |> filter even? |> count # count of matches $xs |> map { $it * 2 } |> reduce 0 { $acc + $it } # fold $xs |> filter { $it > 2 } |> find { $it > 3 } # first match $xs |> map { $it * 2 } |> join "," # stringify $xs |> filter { $it > 2 } |> first # same as `find` without pred $xs |> filter { $it > 2 } |> last
### TODO 隐式求值
`for` 循环、`echo` 和赋值(`set`)会自动对惰性序列求值:
for x [map @{1 2 3} { $it * 2 }] { echo $x } # realises via for-loop echo $xs |> map { $it * 2 } # realises via echo set result $xs |> map { $it * 2 } # realises on assignment
## 流水线与嵌套调用
对于多步转换,流水线是惯用形式:
# ✓ Preferred @{ 1 2 3 } |> map { $it * 2 } |> filter { $it > 2 } |> collect # ✗ Works, but unidiomatic [filter [map @{ 1 2 3 } { $it * 2 }] { $it > 2 }]
流水线形式让每一步从左到右阅读,避免向右漂移。
## 步骤融合
流水线步骤在运行时融合——求值过程只遍历源一次,并对每个元素运行所有转换。不会分配中间集合。
@{ 1 2 3 4 5 }
|> map { $it * 2 }
|> map { $it + 1 }
|> filter { $it > 4 }
|> collect
# one walk, two maps and a filter per element
## 缓存
惰性序列的元素一旦被强制求值,就会被缓存。重新遍历同一结果不会重新运行流水线:
set xs [map @{ 1 2 3 } { expensive-op $it }] echo [$xs first] # runs expensive-op on 1 echo [$xs first] # uses cached result
## 后缀语法糖
管道操作符是最自然的形式,但单步调用也可以在变量上使用后缀形式:
set xs @{ 1 2 3 } set doubled $xs(map { $it * 2 }) set evens $xs(filter { ($it % 2) == 0 })
注意:变量与左括号之间没有空格。`$xs (map ...)` 是语法错误。
## 迭代映射与字符串
`ISeq` 适用于任何序列,而不仅是列表。映射产生 `@{key value}` 对;字符串产生单个字符:
%{ :a 1 :b 2 } |> map { $it(1) } |> collect # => @{ 1 2 }
"hello" |> map { $it(upper) } |> join "" # => "HELLO"
更多内容参见 集合。
## 使用 `take` 提前终止
`[take n]` 是终止无限或超大型流水线的主要工具:
def naturals [iterate { $it + 1 } 1] # if you have iterate # Without iterate, use range with a large bound and take what you need [range 0 1000000] |> filter { ($it % 7) == 0 } |> take 5 |> collect # => @{ 0 7 14 21 28 }
## 何时改用转换器
惰性序列形式非常适合一次性的集合到集合转换。在以下情况下应改用转换器(参见 转换器):
- 同一流水线要应用于多个集合。
- 汇聚端不是向量(集合、映射、求和、自定义 reducer)。
- 希望借助 `%Reduced` 在整个链上显式提前终止。
## 快速参考
| 操作 | 返回值 | 说明 |
|---|---|---|
| `map` | 惰性序列(2 参数) | 对每个元素应用 f |
| `filter` | 惰性序列(2 参数) | 保留 pred 为真的元素 |
| `take` | 惰性序列(2 参数) | 前 n 个元素 |
| `drop` | 惰性序列(2 参数) | 除前 n 个之外的所有元素 |
| `mapcat` | 惰性序列(2 参数) | 映射后拼接结果 |
| `collect` | 列表 | 强制求值惰性序列 |
| `reduce` | 折叠后的值 | 使用初始值和步进函数进行归约 |
| `find` | 第一个匹配或 nil | 查找第一个满足条件的元素 |
| `any?` / `all?` | 布尔值 | 存在性 / 全称性 |
| `group` | 以结果为键的映射 | 按函数分区 |
| `count` | 数字 | 求值后的长度 |
| `first` / `last` | 元素 | 两端 |
| `sort` | 列表 | 排序后的副本 |
| `unique` | 列表 | 去重 |