你好,类型(九):Let polymorphism

类型变量

到目前为止,我们遇到的每一个 \lambda 项都有唯一确定的类型,
因为,项的类型都被显式的注释在了它的后面。
例如,我们可以定义一个恒等函数 id=\lambda x:Nat.~x:Nat\to Nat
id 的类型就是固定的, Nat\to Nat ,而 id~true 就不是良类型的。

为每一个类型的恒等函数都定义各自的版本,是非常繁琐的,
因此,一个自然的想法是,我们能否让 id 的类型参数化
让它在不同的上下文中,实例化为不同的具体类型。
例如, id=\lambda x:X.~x:X\to X ,其中 X类型参量

类型代换

类型代换 \sigma ,指的是一个从类型变量到类型的有限映射。
例如, \sigma=[X\mapsto T,Y\mapsto U] ,会将类型变量 X,Y 分别代换为 T,U
其中, X,Y 称为代换 \sigma定义域,记为 dom(\sigma)
T,U 称为代换 \sigma值域,记为 range(\sigma)

值得一提的是,所有的代换都是同时进行的, \sigma=[X\mapsto Bool,Y\mapsto X\to X]
是将 X 映射成 Bool ,将 Y 映射成 X\to X ,而不是 Bool\to Bool

代换可以用下面的方式来定义,
(1) \sigma(X)=X ,如果 X\notin dom(\sigma)
(2) \sigma(X)=T ,如果 (X\mapsto T)\in\sigma
(3) \sigma(Nat)=Nat\sigma(Bool)=Bool
(4) \sigma(T_1\to T_2)=\sigma T_1\to\sigma T_2

对于类型上下文 \Gamma=\{x_1:T_1,\cdots,x_n:T_n\} 来说, \sigma\Gamma=\{x_1:\sigma T_1,\cdots,x_n:\sigma T_n\}

类型代换的一个重要特性是它保留了类型声明的有效性,
如果包含类型变量的项是良类型的,那么它的所有代换实例也都是良类型的。

类型推断

在类型上下文 \Gamma 中,对于包含类型变量的项 t ,我们通常会提出两个问题,

(1)它的所有代换实例,是否都是良类型的?
即,是否 \forall\sigma\exists T,\sigma\Gamma\vdash\sigma t:T

(2)是否存在良类型的代换实例?
即,是否 \exists\sigma\exists T,\sigma\Gamma\vdash\sigma t:T

对于第一个问题,将引出参数化多态parametric polymorphism),
例如, \lambda f:X\to X.\lambda a:X.f(f(a)) ,它的类型为 (X\to X)\to X\to X
无论用什么具体类型 T 来代换 X ,代换实例都是良类型的。

对于第二个问题,原始的项可能不是良类型的,
但是可以选择合适的类型代换使之实例化为良类型的项。

例如, \lambda f:Y.\lambda a:X.f(f(a)) ,是不可类型化的,
但是如果用 Nat\to Nat 代换 Y ,用 Nat 代换 X
\sigma=[X\mapsto Nat,Y\mapsto Nat\to Nat]
就可以得到, \lambda f:Nat\to Nat.\lambda a:Nat.f(f(a))
可类型化为 (Nat\to Nat)\to Nat\to Nat

或者,取 \sigma'=[Y\mapsto X\to X] ,结果也能得到一个良类型的项,尽管仍包含变量。

在寻找类型变量有效实例的过程中,出现了类型推断type inference)的概念。
意味着由编译器来帮助推断 \lambda 项的具体类型,
ML语言中,程序员可以忽略所有的类型注释——隐式类型(implicit typing)。

在进行推断的时候,对每一个原始的 \lambda 抽象 \lambda x.t
都用新的类型变量进行注释,写成 \lambda x:X.t
然后采取特定的类型推导算法,找到使项通过类型检查的一个最一般化的解。

\Gamma 为类型上下文, t 为项,
(\Gamma,t),是指这样的一个序对 (\sigma,T) ,使得 \sigma\Gamma\vdash\sigma t:T 成立。

例如,设 \Gamma=f:X,a:Yt=f~a ,则
(\sigma=[X\mapsto Y\to Nat],Nat)(\sigma=[X\mapsto Y\to Z],Z) ,都是 (\Gamma,t) 的解。

基于约束的类型化

(1)约束集

在实际情况中, (\Gamma,t) 的解,并不一定满足其他类型表达式的约束条件,
所以,我们寻找的是满足这些约束条件的特解。

所谓约束条件,实际上指的是约束集 C
它由一些包含类型参量的项的等式构成, \{S_i=T_i|i\in l..n\}

如果一个代换 \sigma 的代换实例, \sigma S\sigma T 相同,则称该代换合一(unify)了等式 S=T
如果 \sigma 能合一 C 中的所有等式,则称 \sigma合一(unify)或满足(satisfy) C

我们用 \Gamma\vdash t:T|_\chi C ,来表示约束集 C 满足时,项 t\Gamma 下的类型为 T
其中 \chi 为约束集中,所有类型变量的集合,有时为了讨论方便可以省略它。

例如,对于项 t=\lambda x:X\to Y.x~0
约束集可以写为 \{Nat\to Z=X\to Y\} ,则 t 类型为 (X\to Y)\to Z 。(算法略)
而代换 \sigma=[X\mapsto Nat,Z\mapsto Bool,Y\mapsto Bool] ,使得等式 Nat\to Z=X\to Y 成立,
所以,我们推断出了 (Nat\to Bool)\to Bool 是项 t 的一个可能类型。

(2)约束集的解

约束集的解一般不是唯一的,所以一个关键问题是如何确定一个“最好”的解。

我们称代换 \sigma\sigma' 更具一般性(more general),如果 \sigma'=\gamma\circ\sigma ,记为 \sigma\sqsubseteq\sigma'
其中, \gamma 为一个代换, \gamma\circ\sigma 表示代换的复合, (\gamma\circ\sigma)S=\gamma(\sigma S)

约束集 C主合一子(principal unifier)指的是代换 \sigma
它能满足 C ,且对于所有满足 C 的代换 \sigma' ,都有 \sigma\sqsubseteq\sigma'

如果 (\Gamma,t,S,C) 的解 (\sigma,T) ,对于任何其他解 (\sigma',T') ,都有 \sigma\sqsubseteq\sigma'
则称 (\sigma,T) 是一个主解(principal solution),称 Tt主类型(principal type)。
可以证明,如果 (\Gamma,t,S,C) 有解,则它必有一个主解。

let多态

多态(polymorphism)指的是单独一段程序能在不同的上下文中实例化为不同的类型。
其中let多态,是由let表达式引入的多态性。

(1)单态性

假设我们定义了一个 double 函数,它能将一个函数对参数应用两次,
let~double=\lambda f:Nat\to Nat.\lambda a:Nat.f(f(a))~in
~~~~double~(\lambda x:Nat.succ~x)~1
此时, double 的类型为 (Nat\to Nat)\to Nat\to Nat

如果我们想将 double 应用于其他类型,就必须重写一个新的 double'
let~double'=\lambda f:Bool\to Bool.\lambda a:Bool.f(f(a))~in
~~~~double'~(\lambda x:Bool.x)~true
此时 double' 的类型为 (Bool\to Bool)\to Bool\to Bool

我们不能让一个 double 函数,既能用于 Nat 类型,又能用于 Bool 类型。
即使在 double 中用类型变量也没有用,
let~double=\lambda f:X\to X.\lambda a:X.f(f(a))~in~\cdots

例如,如果写,
let~double=\lambda f:X\to X.\lambda a:X.f(f(a))~in ~~~~let~a=double~(\lambda x:Nat.succ~x)~1~in ~~~~~~~~let~b=double~(\lambda x:Bool.x)~true~in~\cdots
则在 a 定义中使用 double ,会产生一个约束 X\to X=Nat\to Nat
而在 b 定义中使用 double ,则会产生约束 X\to X=Bool\to Bool
这样会使类型变量 X 的求解发生矛盾,导致整个程序不可类型化。

(2)多态性

let多态所做的事情,就是打破这个限制,
让类型参量 X 在上述不同的上下文中,可以分别实例化为 NatBool

这需要改变与let表达式相关的类型推导规则,在第七篇中,我们提到过,
\frac{\Gamma\vdash t_1:T_1~~~~\Gamma,x:T_1\vdash t_2:T_2}{\Gamma\vdash let~x:T_1=t_1~in~t_2:T_2}
它会首先计算 T_1 作为 x 的类型,然后再用 x 来确定 T_2 的类型。
此时,let表达式 let~x=t_1:T_1~in~t_2 ,可以看做 (\lambda x:T_1.t_2)t_1 的简写。

为了引入多态性,我们需要对上述类型推导规则进行修改,
\frac{\Gamma\vdash[x\mapsto t_1]t_2:T_2}{\Gamma\vdash let~x=t_1~in~t_2:T_2}
它表示,先将 t_2 中的 xt_1 代换掉,然后再确定 t_2 的类型。

这样的话,
let~double=\lambda f:X\to X.\lambda a:X.f(f(a))~in ~~~~let~a=double~(\lambda x:Nat.succ~x)~1~in ~~~~~~~~let~b=double~(\lambda x:Bool.x)~true~in~\cdots

就相当于,
let~a=\lambda f:X\to X.\lambda a:X.f(f(a))~(\lambda x:Nat.succ~x)~1~in ~~~~let~b=\lambda f:Y\to Y.\lambda a:Y.f(f(a))~(\lambda x:Bool.x)~true~in~\cdots
通过let多态,产生了 double 的两个副本,并为之分配了不同的类型参量。

此时,let表达式 let~x=t_1~in~t_2 ,可以看做 [x\mapsto t_1]t_2 的简写。


参考

Hindley–Milner type system
Types and programming languages
Haskell 2010 Language Report


下一篇:你好,类型(十):Parametric polymorphism

文章被以下专栏收录