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

10 · 可变状态:识别共享并限制修改范围

目标:识别数组与引用的共享关系,并让可变累加器局限在一次函数调用内部。

OCaml 支持可变状态,也支持 for、while。函数式编程不是禁止赋值,而是让数据变化尽量容易推理。不可变记录更新会产生新记录;数组赋值则直接修改已有对象。如果两个名字指向同一数组,一个名字的修改能被另一个名字看到。

ref 0 建立保存整数的可变单元,!cell 读取,cell := value 写入。名字 cell 的绑定本身没有变化,变化的是单元里的内容。数组用 [| ... |] 构造,a.(i) 读取,a.(i) <- value 写入,下标从 0 开始。

实验:把共享与复制分开观察

创建 mutation.ml:

let sum_array values =
  let total = ref 0 in
  Array.iter (fun n -> total := !total + n) values;
  !total

let () =
  let original = [|25; 35|] in
  let shared = original in
  let copied = Array.copy original in
  shared.(0) <- 10;
  Printf.printf "original=%d copied=%d\n" original.(0) copied.(0);
  Printf.printf "sum=%d again=%d\n"
    (sum_array original) (sum_array original)

先预测 original.(0) 是否还为 25,再运行:

ocaml mutation.ml

预期输出:

original=10 copied=25
sum=45 again=45

shared 只是另一个名字;Array.copy 才建立独立的外层数组。本例元素是整数,所以复制后修改元素互不影响。数组若包含 ref 等可变值,外层复制仍会共享这些内部对象,不是通用深拷贝。

sum_array 每次调用都创建自己的 total,不修改输入,连续调用没有状态残留。这个例子展示局部可变实现如何保留简单接口;StudyLog 的列表汇总用 fold_left 已足够,无须为了“使用新语法”改成数组。

类型推导为什么有时显得保守

第 3 课的 identity 可以用于不同类型,但同一个可变单元不能这次装整数、下次装字符串,还让读取方以为它始终是原来的类型。若你在交互解释器中看见 '_weak1 一类弱类型变量,它表示类型尚未确定且不能任意泛化;首次约束后会固定。不要把它当成 identity 的自由类型参数。更完整的值限制规则见文末手册,本课只要求理解共享可变内容带来的风险。

  1. 将赋值改为 copied.(0) <- 10,原数组应保持 25,总和为 60。
  2. 把 total 移到函数外再连续调用。结果会积累,调用之间不再独立。不要依赖打印参数的求值顺序判断哪一个先执行;可以先分别用 let first = ... in let second = ... in 保存结果。
  3. 恢复主示例,使 total 回到函数内部。

独立练习:实现 increased_copy extra values,返回每项增加 extra 的新数组,原数组不变。使用 Array.map:它与 List.map 类似,但产生新数组。

参考实现

追加并运行:

let increased_copy extra values = Array.map (fun n -> n + extra) values
let () =
  let before = [|25; 35|] in
  let after = increased_copy 5 before in
  assert (before = [|25; 35|]);
  assert (after = [|30; 40|]);
  assert (increased_copy 5 [||] = [||]);
  print_endline "mutation checks passed"

若出现数组下标异常,检查 Array.length 与从 0 开始的下标;最后一个合法位置是长度减一。若第二次运行函数结果比第一次大,检查是否把累加器放在了共享作用域。数组、引用和可变字段都应明确谁能修改、修改何时发生。

参考资料