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)。
实验变奏
- 把
daily_remaining的目标改成 90。left应变为 65。 - 将
twice的实参改成fun x -> x * 2。twice一行应打印 40。 - 恢复主示例。用一句话解释:为什么
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 都当成随时可变的任意类型。