Programming Languages, UW, Part C - Subtyping
我们定义某个 static type system 的expressiveness (表达性) 为:not rejecting too many programs that do nothing wrong and programmers are likely to write。
- ML type system 的表达性来自 parametric polymorphism (参数多态),即 generics (泛型)。
- 许多 OOP style (Java, C#) 的 type system 的表达性更多的来自于 subtype polymorphism (子类型多态),也即我们所说的 subtyping。
我们将使用一种类 ML 的 MUPL 来介绍 subtyping;但请注意,ML 本身是不支持 subtyping 的。
This article is a self-administered course note.
It will NOT cover any exam or assignment related content.
Introduction and Rules
我们来看一个例子:
1 | fun distToOrigin(p:{x:real, y:real}) = |
该程序是一个典型的 false positive (假阳性) 程序。在不支持 subtyping
的语言中,将 c
传入 distToOrigin
方法将会产生
type error。这引出了 subtyping 的必要性:它在一定程度上令 type system
更加 complete,从而提高了其 expressiveness。(关于 type system
的分析见 Part
B)
c
的类型 {x:real, y:real, color:string}
是形参 p
类型 {x:real, y:real}
的子类型,而
subtype can have any of its supertypes,所以 c
能够顺利传入
distToOrigin
方法中。
Width Subtyping
对于类型 t1
与 t2
,若 t1
是
t2
的 subtype,我们标记为 t1 <: t2
。那么
subtyping 可以表示为这么一条最基本的 typing tule:If e
has
type t1
and t1 <: t2
, then e
(also) has type t2
.
为了进一步提升 type system 的 expressiveness,还有四条
rules;其中最重要的是对 <:
关系本身的定义。最常见的一种定义是 width subtyping,即
supertype 与 subtype 呈宽度展开关系:在 width subtyping 中,supertype
中含有的 fields 一定是 subtype 的子集。
- Width subtyping. A supertype can have a subset of fields
with the same type, i.e., a subtype can have extra fields:
{f1:t1,...,fm:tm,...,fn:tn} <: {f1:t1,...,fm:tm}
. - Permutation subtyping. A supertype can have the same set of fields
with the same types in a different order:
{y:real, x:real} <: {x:real, y:real}
. - Transitivity. If
t1 <: t2
andt2 <: t3
, thent1 <: t3
. - Reflexitivity. Every type is a subtype of itself:
t <: t
.
结合 width subtyping, permutation subtyping 与
transitivity,我们能够做到类似
{x:real, foo:string, y:real} :< {y:real, x:real}
的子类型关系,这大大提高了 type system 的 expressiveness。
Depth Subtyping: A Bad Idea With Mutation
在使用 subtyping 时,最常见的错误如下:
1 | fun circleY (c:{center:{x:real, y:real}, r:real}) = |
circleY
方法返回传入圆的圆心的 y
坐标。自然的,对于一个球 sphere,我们可能会认为其可以作为圆 circle
的子类型传入 circleY
方法,并成功得到球心的 y 坐标。
实际上,circle 类型与 sphere 类型之间的关系并不遵循 width subtyping 规则:circle 类型的 fields 并不是 sphere 类型的子集,因此该程序将无法 type check。但抛开圆与球类型继承上的逻辑问题,该程序本质上是 false positive 的:它在运行时并不会出现 field missing 的错误。
为了进一步提升 type system 的 expressiveness,不妨再添加一条 subtyping rule,称为 depth subtyping:
- Depth subtyping: If
ta <: tb
, then{f1:t1,...,f:ta,...,fn:tn} <: {f1:t1,...,f:tb,...,fn:tn}
.
在该规则下,以上的程序就能 type-check 了。
遗憾的是,depth subtyping 这一规则会破坏我们的 type system;一些 false negative 的程序将通过 type check。这也是程序员常犯的错误之一:认为 depth subtyping 是被允许的。见下例:
1 | fun setToOrigin (c:{center:{x:real, y:real}, r:real}) = |
在允许 depth subtyping 的语言中,该程序能通过 type
check。但在运行时,最后一行 sphere.center.z
将出现 field
missing 错误;这是由于 setToOrigin
方法 mutate
其参数,使得 center
失去了 z
域。
对于一个支持 subtyping 的语言,以下三个 features 最多只能同时做到其中两个:
- Fields of objects are settable (i.e., immutable).
- Supporting depth subtyping.
- Having a type system actually prevent field-missing errors.
Array Subtyping
我们来看一段 Java 代码,其在 Java 中是 type check 的。
1 | class Point { ... } // has fields double x, y |
这样的操作称为 array subtyping:即 If
ta <: tb
, then
ta[] <: tb[]
。它本质上就是 depth
subtyping。根据上文中我们总结的规律,Java 支持三个 features
中的两个:
- Array subtyping.
- Array indices are mutable.
那么 Java 的 type system 就无法阻止某些程序的 field-missing 错误。事实确实如此,上述程序能够通过 Java 的 type check。为了解决这个问题,Java 引入了 run-time checks。
于是当程序运行到 pt_arr[0] = new Point(3, 4)
这一语句时,Java 的 run-time check 会发现 ColorPoint[]
类的数组存储了其 supertype Point
类的对象,于是
ArrayStoreException
错误将被抛出。
Java 与 C# 以加入 run-time checks 的代价换取了 depth subtyping,这是因为 depth subtyping 自有其意义:它的确提升了语言的灵活性 (flexibility) 与 type system 的 expressiveness。
除了利用 run-time checks 之外,还有其他的方法可以达到相同的目的:
- Bounded polymorphism. Use generics (parametric polymorphism) combined with subtyping.
- Having support for indicating that a method will not update array elemtents, in which case depth subtyping is sound.
Function Subtyping
函数 (方法) 也拥有它的类型,因此研究函数类型间的 subtyping 关系也是很有必要的。It is just as important for understanding how to safely override methods in object-oriented languages.
When we talk about function subtyping, we are talking about using a function of one type in place of a function of another type.
我们以 higher-order function 为例研究函数类型间的 subtyping
关系:distMoved
function computes the distance between the
two-dimensional point p
and the result of calling
f
with p
.
1 | fun distMoved (f : {x:real,y:real}->{x:real,y:real}, |
那么函数 distMoved
的类型为
(({x:real,y:real}->{x:real,y:real}) * {x:real,y:real}) -> real
.
函数 flip
的类型为
{x:real, y:real} -> {x:real, y:real}
,与函数
distMoved
期待的参数类型是相符的;以函数 flip
为参数调用函数 distMoved
没有用到 subtyping。
Convariant Return Type
函数类型与其返回值类型的 subtyping 关系是 covariant (协变) 的: If
ta <: tb
, then
t -> ta <: t -> tb
.
1 | fun flipGreen p = {x=~p.x, y=~p.y, color="green"} |
如上例,flipGreen
函数作为参数传入
distMoved
函数不会产生任何问题。
flipGreen
函数的返回值类型为{x:real,y:real,color:string}
,是distMoved
形参函数返回值类型{x:real,y:real}
的 subtype。- 而
flipGreen
函数类型{x:real,y:real}->{x:real,y:real,color:string}
是distMoved
形参函数类型{x:real,y:real}->{x:real,y:real}
的 subtype。
可以看出函数类型与其返回值类型 subtyping 的协变关系。
Contravariant Argument Type
函数类型与其参数类型的 subtyping 关系是 contravariant (逆变) 的: If
ta <: tb
, then
tb -> t <: ta -> t
.
1 | fun flipX_Y0 p = {x=~p.x, y=0} |
如上例,flipX_Y0
函数作为参数传入 distMoved
函数不会产生任何问题。
flipX_Y0
函数的参数类型为{x:real}
,是distMoved
形参函数参数类型{x:real,y:real}
的 supertype。- 而
flipX_Y0
函数类型{x:real}->{x:real,y:real}
是distMoved
形参函数类型{x:real,y:real}->{x:real,y:real}
的 subtype。
可以看出函数类型与其参数类型 subtype 的逆变关系。
注意在 OOP 语言中,self
(或 this
)
是被特殊处理的,它的类型与对象的类型始终是 convariant (共变) 的。
Subtyping for OOP
在 Java 与 C# 这些 statically typed OOP 语言中存在许多容易混淆的概念,如 inheritance v.s. subtyping, class v.s. type 等,这一节将对其进行澄清。(很重要的一节!)
在之前,我们使用 ML-like MUPL 中的 record feature 来解释了 subtyping。其实对于 OOP 中的 object,我们也可以视为一个 record:An object is basically a record holding fields (which we assume here are mutable) and methods (we assume the "slots" for methods are immutable).
结合之前我们基于 record 与 higher-order function 研究的 width typing 与 function typing,我们定义 OOP 中的 subtyping 规则如下:
- (Width typing for fields) A subtype can have extra fields.
- (NO depth typing for fields) Because fields are mutable, a subtype cannot have a different type for a field.
- (Width typing for methods) A subtype can have extra methods.
- (Depth typing for methods) Because methods are immutable, a subtype can have a subtype for method, which means the method in the subtype can have contravariant argument types and a covariant result type.
虽然我们可以将 object 视为 PL 中的 record,Java 与 C# 中的 object types 与 ML-like 的 record types,function types 却有着很大的不同。
Subtyping in Java and C# includes only the subtyping explicitly stated via:
- the subclass relationship.
- the interfaces that classes explicitly indicate they implement (including interfaces implemented by superclasses).
接下来我们介绍 OOP 中的 subclassing 规则:它比 subtyping 规则更为严格,因此可以 soundly 地阻止 field missing 与 method missing 错误。
- A subclass can add fields but not remove them.
- A subclass can add methods but not removed them.
- A subclass can override a method with a covariant return type. (在 C++ 中,这条规则更为严格,重写方法地参数表与返回值地类型均不能改变)
- A class can implement more methods than an interface requires or implement a required method with a covariant return type.
注意,classes 与 types 是两个完全不同的概念!!!
在 C# 与 Java 这种语言中,每个 class declaration 在定义 class 的同时引入同名的 type。为了方便, classes 与 types 的界限被刻意模糊了。类似的,inheritance (subclassing) 与 subtyping 的界限也变得很暧昧。
但实际上这些概念是有本质上的区别的。(高能预警,接下来的内容很精华,就不翻译了)
Generics v.s. Subtyping
关于 generics (parametric polymorphism) 与 subtyping polymorphism 的比较也是一个很重要的话题。
What are generics good for?
先来看看 generics (parametric polymorphism) 的两种最常见的应用。
higher-order function 的复合。
1
val compose : ('b -> 'c) * ('a -> 'b) -> ('a -> 'c)
functions that operate over collections/containers where different collections/containers can hold values of different types.
1
2
3val length : 'a list -> int
val map : ('a -> 'b) * 'a list -> 'b list
val swap : ('a * 'b) -> ('b * 'a)
generic types
放松了对函数参数类型的限制,从而提高了代码的复用性。举例来说,如果没有
generics,那么对于每一对不同类型的 pair 都需要编写一个 swap
函数。
Subtyping is bad substitute for generics
在不支持 generics 的语言中,程序员有时会用 subtyping 实现 generics,但是:
- Somewhat lame generics.
- Doing so is like painting with a hammer instead of a paintbrush.
1 | class LamePair { |
这是用 subtyping 实现 generics 的一个例子:由于 Object
类型是所有类型的 supertype,任何类型的数据都能传入 LamePair
的域中。问题出在从 LamePair
中提取数据:我们只能获得一个
Object
类型的数据,因此必须使用强制类型转换 downcast
来将其转换为“原本”的类型。
downcast 将对被转换的数据类型执行 run-time checks,因此上述程序中的类型错误只有在运行时才能被侦测到;这一方面增加了动态检查的时空成本,一方面潜在降低了程序的安全性。
What is subtyping good for?
subtyping 的优势区间在于 allowing code to be reused with data that has extra information.
1 | fun distToOrigin {x=x, y=y} = |
该代码在不支持 subtyping 的语言如 ML 中将不会 type check。而在支持
subtyping 的语言中,distToOrigin
函数既能作用于
Point
类的对象,又能作用于 ColorPoint
类的对象:其中 ColorPoint
is data that has extra
information for Point
.
Bounded Polymorphism
既然 generics 与 subtyping 都有着各自的优势区间,许多 statically typed 语言 (例如 Java 与 C#) 选择同时实现这 generics 与 subtyping。同时拥有这两种 polymorphism 机制带来了:
- Some complications. (e.g. static overloading & subtyping are more difficult to define)
- Merits brought by generics and subtyping separately.
- More code reuse and expressiveness with the combination of the two ideas.
generics 与 subtyping 的结合带来了 bounded polymorphism, 具体体现为 bounded generic types。
见下例,Point
类中定义了 distance
方法。
1 | class Pt { |
接下来是一个静态函数,输入一个 point
类 list
与一个圆,返回 list 中所有在该圆内的 point
。
1 | static List<Pt> inCircle(List<Pt> pts, Pt center, double radius) { |
对于一个 ColorPt
类型的 list,我们能否将其传入
inCircle
方法实现代码的复用?该问题我们已经讨论过。
- 一方面,list 中的各个 entry 是 mutable 的,这意味着 depth subtyping
不成立,
List<ColorPt>
并非List<Pt>
的 subtype。 - 另一方面,即使
List<ColorPt>
传入了inCircle
方法,其返回的 list 类型是List<Pt>
而不是List<ColorPt>
,我们必须用到丑陋的 downcast。
好消息是,Java 的 bounded polymorphism 使得我们能够结合 generics 与 subtyping,实现该功能:
1 | static <T extends Pt> List<T> inCircle(List<T> pts, Pt center, double radius) { |
利用 generics,我们将 T
声明为一个 generic type;利用
subtyping,我们规定该 generic type T
必须是 Pt
的 subtype。当传入 List<ColorPt>
时,T
将被指定为 ColorPt
,且 ColorPt
的确是
Pt
的 subtype。
这样,我们不仅能够传入 List<ColorPt>
类型的
list,该方法也将返回相同类型的 list 而无需用到 downcast。
Reference
This article is a self-administered course note.
References in the article are from corresponding course materials if not specified.
Supplementry notes:
Inheritance 与 subtyping 到底有什么区别 -知乎.
Course info:
Programming Languages, Part C, University of Washington, Lecturer: Professor Dan Grossman.