OCaml无ref场景下为何部分列表反转函数被推断为弱类型
这个现象和代码里有没有用ref没直接关系,是OCaml类型系统里*值限制(Value Restriction)*的保守判定规则导致的。
判定规则说明
OCaml判断一个let绑定能不能拿到通用多态类型,只做最表层的语法检查,根本不会深入分析表达式纯不纯、有没有副作用:只有等号右边的表达式属于语法值的时候,才会直接给通用多态类型。
语法值的范围非常明确:
- 常量、已绑定的变量是语法值
- 显式写出来的函数(也就是
fun x -> ...形式的λ抽象)是语法值 - 无副作用的数据构造器套在语法值上,也算语法值
除此之外的所有表达式,哪怕是纯函数算出来的结果,都不算语法值,拿不到通用多态。
你写的两个反转函数刚好踩在判定边界两边:
第一个版本的代码:
let rev = let rec impl_rec acc = function | [] -> acc | x::xs -> impl_rec (x::acc) xs in impl_rec []
等号右边是impl_rec []——这是个函数调用表达式,哪怕这个调用100%纯、返回的就是个反转函数,它也不属于语法值,所以编译器不会给它通用多态,只会推断出带弱类型标记的类型'_weak list -> '_weak list。
第二个版本的代码:
let rev xs = let rec impl_rec acc = function | [] -> acc | x::xs -> impl_rec (x::acc) xs in impl_rec [] xs
本质是显式的函数定义,等价于let rev = fun xs -> impl_rec [] xs,属于标准的λ抽象语法值,所以编译器直接给了通用多态类型'a list -> 'a list。
弱类型到底是什么
这里的'_weak根本不是通用多态变量,它是个等着被固定的单态占位符:
第一个版本的rev第一次被调用的时候,传入参数的类型会直接把'_weak钉死成具体类型,之后这个rev就只能处理对应类型的列表。比如你第一次跑rev [1;2;3],之后rev的类型就永远是int list -> int list,再传["a";"b"]这种字符串列表直接报类型错。
你写的第二个版本其实就是给第一个版本做了层eta展开,把函数调用返回的闭包外包了一层显式函数,把整个右值变成了符合要求的语法值,自然就绕过了值限制的保守判定,拿到了通用多态。
常见误区
很多人以为值限制只管ref这类可变值,其实不是。值限制最早确实是为了堵可变状态带来的类型安全漏洞——比如let r = ref []要是给了'a list ref的多态类型,完全可以先塞个int列表进去,再取出来当字符串列表用,直接把类型系统冲烂。但OCaml实现这个规则的时候选了最保守的语法判断方案,根本不查表达式内部到底有没有可变状态、有没有副作用,只要右值不在语法值列表里,就一律不给通用多态,所以哪怕是纯函数返回闭包这种完全安全的场景,也会被推断出弱类型。
内容的提问来源于stack exchange,提问作者Willi

