Scala带存在类型的列表:为何map{ case t => ... }可行而map{ t => ... }不行?
咱们先把问题的核心拎出来:这事儿本质是Scala编译器对存在类型的处理逻辑差异,尤其是模式匹配(case)怎么帮编译器“记住”类型之间的关联关系。
先回顾一下咱们的定义
首先看这个存在类型:
type T = (X => X, X) forSome { type X }
每个T实例其实是一对儿:一个从X到X的函数,加上一个X类型的值。但注意,这里的X是每个T实例自己私有的类型——编译器不会默认认为不同实例的X是同一个,甚至同一个实例的两个部分,在普通绑定下也可能被编译器当成“没关系的存在类型”。
为什么普通的x => x._1(x._2)会失败?
当你写list.map{ x => x._1(x._2) }时,编译器对x的认知是:
x._1的类型是(X => X) forSome { type X }(某个未知类型X的函数)x._2的类型是X forSome { type X }(某个未知类型X的值)
重点来了:这两个X是独立的存在量化!编译器没法确定这两个X是同一个——它会觉得你可能拿了一个Y类型的值,传给了一个X=>X的函数,类型自然不匹配,所以直接报错。
为什么加了case就好使了?
当你写成list.map{ case x => x._1(x._2) }时,模式匹配的机制触发了编译器的特殊处理:
编译器会为每个匹配到的T实例,重新引入一个局部的、统一的类型变量X。也就是说,在这个case的作用域里,编译器明确知道:“这个x里的x._1是X=>X,x._2是X,而且这俩X是同一个!”
哪怕你只是写了最简单的case x,编译器也会自动解构这个存在类型,把内部两个部分的类型绑定到同一个X上,这样x._1(x._2)就完全符合类型要求了——函数的输入类型和参数类型完全一致,自然能正常编译运行。
如果把这个case拆成更明确的模式,比如:
list.map{ case (f, v) => f(v) }
效果是一样的——编译器会自动推导,这里的f和v共享同一个存在类型X,所以函数调用合法。
一句话总结
普通的lambda参数绑定只是把存在类型实例当成一个黑盒,编译器没法看穿内部的类型关联;而模式匹配(哪怕是最基础的case)会让编译器主动解构存在类型,把内部的类型变量统一绑定,从而让函数调用的类型检查通过。
内容的提问来源于stack exchange,提问作者Andrey Tyukin

