07 · 高阶函数:用 map、filter 和 fold 处理记录
目标:根据数据处理的输出形状,选择 map、filter 或 fold 完成一条统计流水线。
上一课手写递归让你看到了遍历机制。日常代码里,许多遍历有固定形状:每项变成另一项用 map;保留满足条件的项用 filter;把一整组数据合并成一个结果用 fold_left。先确定输出形状,比先想循环怎么写更清楚。
List.fold_left f initial xs 从左到右把累加结果传给 f。f 接收旧累加器和当前元素,返回新累加器。累加器可以是整数,也可以是 record 或另一种集合。若操作只有打印效果,用 List.iter;用 map 打印会无意义地构造一个 unit list。
如果你已经熟悉 JS 的 map、filter、reduce,可以把精力放在 OCaml 回调的类型和参数顺序上:本课的 fold_left 显式给出初始值,回调先接累加器,再接元素。Python 列表推导式的“筛选后转换”也能帮助理解这条流水线;在 OCaml 中,每一步的输入与输出类型都必须与下一步接得上。
实验:找出足够长的学习记录
创建 pipelines.ml:
type session = { topic : string; minutes : int }
let sessions = [
{ topic = "OCaml"; minutes = 25 };
{ topic = "Types"; minutes = 45 };
{ topic = "OCaml"; minutes = 35 };
]
let total sessions =
List.fold_left (fun acc s -> acc + s.minutes) 0 sessions
let () =
let selected = List.filter (fun s -> s.minutes >= 30) sessions in
let names = List.map (fun s -> s.topic) selected in
print_endline (String.concat ", " names);
Printf.printf "selected=%d all=%d\n" (total selected) (total sessions)
String.concat 用分隔符连接字符串列表。先预测输出中会不会出现两次 OCaml,再运行:
ocaml pipelines.ml
预期输出:
Types, OCaml
selected=80 all=105
过滤不会修改原列表,所以 total sessions 仍为 105。map 保留元素顺序,但把每个 session 变成 string。求和的累加器从 0 开始,经过 45、35 后变成 80;它是已经处理过的记录的分钟数总和。
管道操作符 x |> f 只是 f x 的另一种书写方式。在当前文件中,下面是计算 names 的等价替换片段:
let names =
sessions
|> List.filter (fun s -> s.minutes >= 30)
|> List.map (fun s -> s.topic)
管道没有引入新的执行模型。它依赖部分应用,让数据作为最后一个参数传入。初学时可以把每一步的类型写在纸上:session list -> session list -> string list。
改动阈值,检查空结果
- 将阈值改成 40,只有
Types留下,总计 45。 - 改成 100,第一行为空行,
selected=0 all=105。空集合不是异常。 - 恢复阈值 30。比较“筛选后求和”和“先把所有分钟加起来”为什么不同。
独立练习:实现 total_for topic sessions,统计指定主题的总分钟数。要求 OCaml 得 60,Rust 得 0。不要修改传入的数据。
参考实现与验收
追加到主文件:
let total_for topic sessions =
sessions
|> List.filter (fun s -> s.topic = topic)
|> total
let () =
assert (total_for "OCaml" sessions = 60);
assert (total_for "Rust" sessions = 0);
print_endline "pipeline checks passed"
这里先过滤再求和,易读且足够用于小文件。若要一次算出所有主题的总量,第 17 课会用 Map 一次累积,避免逐主题重复扫描。
如果 fold_left 报类型不匹配,先检查回调的顺序是“累加器、元素”。如果得到 unit list,检查是否把打印放进了 map。若 total 得到函数而非整数,检查流水线是否真的传入了列表,而不是只构造了一个部分应用函数。