Course Builder个人学习工作台 · 本地模式
OCaml:从类型推导到可测试的命令行工具

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。

改动阈值,检查空结果

  1. 将阈值改成 40,只有 Types 留下,总计 45。
  2. 改成 100,第一行为空行,selected=0 all=105。空集合不是异常。
  3. 恢复阈值 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 得到函数而非整数,检查流水线是否真的传入了列表,而不是只构造了一个部分应用函数。

参考资料