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

03 · 函数与类型:读懂柯里化和部分应用

目标:读懂函数签名,并通过部分应用生成一个可复用的时长判断函数。

函数体的最后一个表达式就是返回值,不需要 return。let remaining target spent = ... 看起来接收两个参数,但它的类型是 int -> int -> int,应读作 int -> (int -> int):接收目标,返回一个接收已用时长的函数。这种形式称为柯里化。

因此,remaining 60 已经是合法表达式,只是结果仍然是函数。调用用空格分隔,remaining 60 25 等价于 (remaining 60) 25。相反,f (60, 25) 是把一个二元组作为单个参数传入;第 4 课再学习元组。

用 JS 闭包作心智翻译,柯里化形式更接近 target => spent => ...,调用方式对应 remaining(60)(25);这只是语法示意,不是需要运行的新实验。Python 中固定部分参数的经验可以对应 functools.partial,但这里无需调用工具函数,OCaml 的函数类型本身已经允许分步应用。关键是读懂箭头右结合,而不是把空格机械替换成逗号。Python:functools.partial

类型推导来自约束,不是猜测。使用 - 会约束两侧为 int;一个没有查看参数内容的恒等函数则能适用于多种类型。签名中的 'a 表示类型变量,同一个签名内多次出现的 'a 必须一致。

Go 的 := 可以作为“省略类型但仍受静态检查”的起点;OCaml 这里进一步从函数体推导参数与结果。'a -> 'a 也不能按 Go 的 any 或 JS 的动态值理解:它承诺输入输出是同一种类型,而不是允许运行时随意换成另一种。

实验:固定目标,保留变化的参数

创建 functions.ml:

let remaining target spent =
  if spent >= target then 0 else target - spent

let daily_remaining = remaining 60
let identity x = x
let twice f x = f (f x)
let add_five x = x + 5

let () =
  Printf.printf "left=%d\n" (daily_remaining 25);
  Printf.printf "twice=%d\n" (twice add_five 10);
  print_endline (identity "OCaml")

先预测 daily_remaining 是整数还是函数,再运行并查看推导出的接口:

ocaml functions.ml
ocamlc -i functions.ml

第一条命令的预期输出:

left=35
twice=20
OCaml

第二条命令的预期输出:

val remaining : int -> int -> int
val daily_remaining : int -> int
val identity : 'a -> 'a
val twice : ('a -> 'a) -> 'a -> 'a
val add_five : int -> int

twice 的参数 f 本身是函数,所以它是高阶函数。第一次 f x 的结果又传给同一个 f,迫使输入输出类型一致。identity 可以单独用于整数,也可以单独用于字符串,但它不会把整数变成字符串。

匿名函数用 fun x -> ... 表示。例如,在当前示例内,twice (fun x -> x + 5) 10 可以替换 twice add_five 10。函数调用比中缀加法结合得更紧;若要把 10 + 5 整体作为输入,应写 daily_remaining (10 + 5)。

实验变奏

  1. 把 daily_remaining 的目标改成 90。left 应变为 65。
  2. 将 twice 的实参改成 fun x -> x * 2。twice 一行应打印 40。
  3. 恢复主示例。用一句话解释:为什么 twice string_of_int 10 不能成立?

独立练习:定义 at_least minimum actual,返回是否达到目标;再定义 long_session = at_least 45,验证 44 为假、45 为真。不写重复的比较代码。

参考答案

把下面代码追加到主文件后,再执行同一条 ocaml 命令:

let at_least minimum actual = actual >= minimum
let long_session = at_least 45
let () =
  assert (not (long_session 44));
  assert (long_session 45);
  print_endline "function checks passed"

assert 的条件为假时抛出异常,为真时返回 ()。新增的最后一行应为 function checks passed。

string_of_int 是 int -> string,第一次应用后得到字符串,第二次却还需要整数,因此不能传给要求 'a -> 'a 的 twice。这里的类型约束在运行前就发现了组合问题。

如果错误说“不是函数,不能应用”,检查括号是否把整个算式包成了一个参数。如果打印时说“这里得到一个函数”,检查是否少传了参数。后续第 10 课会提醒:包含可变状态的值并不总能像 identity 一样泛化,不能把所有 'a 都当成随时可变的任意类型。

参考资料