Lecture 26
Subtyping
Programming Languages
UW CSE 341 - Spring 2021
Last major topic: Subtyping
Build up key ideas from first principles
Then in next lecture:
A tiny language
records with mutable fields
Records (half like ML, half like Java)
Record creation (field names and contents):
Evaluate ei, make a record
Record field access:
Evaluate e to record v, get contents of v‘s f field
Record field update:
Evaluate e1 to a record v1 and e2 to a value v2;
Change v1's f field (which must exist) to v2;
Return v2
{f1=e1, f2=e2, …, fn=en}
e.f
e1.f = e2
A Basic Type System
Record types: What fields a record has and type for each field
Unlike ML/Java: Not using named types (complicates discussion)
Type-checking expressions:
then {f1=e1, …, fn=en} has type {f1:t1, …, fn:tn}
then e.f has type t
then e1.f = e2 has type t
{f1:t1, f2:t2, …, fn:tn}
This is sound
These evaluation rules and typing rules prevent ever trying to access a field of a record that does not exist
Example program that type-checks (in a made-up OCaml-like language):
let distToOrigin (p:{x:float,y:float}) =
Float.sqrt(p.x*p.x + p.y*p.y)
let pythag : {x:float,y:float} = {x=3.0, y=4.0}
let five : float = distToOrigin(pythag)
Motivating subtyping
But according to our typing rules, this program does not type-check
let distToOrigin (p:{x:float,y:float}) =
Float.sqrt(p.x*p.x + p.y*p.y)
let c : {x:float,y:float,color:string} =
{x=3.0, y=4.0, color="green"}
let five : float = distToOrigin(c)
A good idea: allow extra fields
Natural idea: If an expression has type
{f1:t1, f2:t2, …, fn:tn}
Then it can also have a type with some fields removed
let distToOrigin (p:{x:float,y:float}) = …
let makePurple (p:{color:string}) =
p.color = "purple"
let c :{x:float,y:float,color:string} =
{x=3.0, y=4.0, color="green"}
let _ = distToOrigin(c)
let _ = makePurple(c)
Keeping subtyping separate
A programming language already has a lot of typing rules and we do not want to change them
We can do this by adding “just two things to our language”
If e has type t1 and t1 <: t2,
then e (also) has type t2
Now all we need to do is define t1 <: t2
Subtyping is not a matter of opinion
Four good rules
For our record types, these rules all meet the substitutability test:
(4) may seem unnecessary, but it composes well with other rules in a full language and “does no harm”
More record subtyping?
[Warning: I am misleading you ☺]
Subtyping rules so far let us drop fields but not change their types
Example: A circle has a center field holding another record
For this to type-check, we need:
{center:{x:float,y:float,z:float}, r:float}
<: {center:{x:float,y:float}, r:float}
let circleY (c:{center:{x:float,y:float}, r:float}) =
c.center.y
let sphere:{center:{x:float,y:float,z:float}, r:float} = {center = {x=3.0,y=4.0,z=0.0}, r = 1.0}
let _ = circleY(sphere)
Do not have this subtyping – could we?
{center:{x:float,y:float,z:float}, r:float}
<: {center:{x:float,y:float}, r:float}
If ta <: tb, then {f1:t1, …, f:ta, …, fn:tn} <:
{f1:t1, …, f:tb, …, fn:tn}
Stop!
Mutation strikes again
If ta <: tb,
then {f1:t1, …, f:ta, …, fn:tn} <:
{f1:t1, …, f:tb, …, fn:tn}
CSE341: Programming Languages
15
Spring 2019
let setToOrigin (c:{center:{x:float,y:float},
r:float}) =
c.center = {x=0.0,y=0.0}
let sphere:{center:{x:float,y:float,z:float},
r:float} =
{center = {x=3.0,y=4.0,z=0.0}, r = 1.0}
let _ = setToOrigin(sphere)
let _ = sphere.center.z (* kaboom! no z field!:( *)
Moral of the story
Picking on Java (and C#)
Arrays should work just like records in terms of depth subtyping
class Point { … }
class ColorPoint extends Point { … }
…
void m1(Point[] pt_arr) {
pt_arr[0] = new Point(3,4);
}
String m2(int x) {
ColorPoint[] cpt_arr = new ColorPoint[x];
for(int i=0; i < x; i++)
cpt_arr[i] = new ColorPoint(0,0,"green");
m1(cpt_arr); // !
return cpt_arr[0].color; // !
}
Why did they do this?
So what happens
void m1(Point[] pt_arr) {
pt_arr[0] = new Point(3,4); // can throw
}
String m2(int x) {
ColorPoint[] cpt_arr = new ColorPoint[x];
…
m1(cpt_arr); // "inappropriate" depth subtyping
ColorPoint c = cpt_arr[0]; // fine, cpt_arr
// will always hold (subtypes of) ColorPoints
return c.color; // fine, a ColorPoint has a color
}
null
Now functions
Example
No subtyping here yet:
let distMoved (f : {x:float,y:float}->{x:float,y:float},
p : {x:float,y:float}) =
let p2 : {x:float,y:float} = f p in
let dx : float = p2.x – p.x in
let dy : float = p2.y – p.y in
Float.sqrt(dx*dx + dy*dy)
let flip p = {x = -p.x, y=-p.y}
let d = distMoved(flip, {x=3.0, y=4.0})
Return-type subtyping
let distMoved (f : {x:float,y:float}->{x:float,y:float},
p : {x:float,y:float}) =
let p2 : {x:float,y:float} = f p in
let dx : float = p2.x – p.x in
let dy : float = p2.y – p.y in
Float.sqrt(dx*dx + dy*dy)
let flipGreen p = {x = -p.x, y=-p.y, color="green"}
let d = distMoved(flipGreen, {x=3.0, y=4.0})
This is wrong
let distMoved (f : {x:float,y:float}->{x:float,y:float},
p : {x:float,y:float}) =
let p2 : {x:float,y:float} = f p in
let dx : float = p2.x – p.x in
let dy : float = p2.y – p.y in
Float.sqrt(dx*dx + dy*dy)
let flipIfGreen p = if p.color = "green" (*kaboom!*)
then {x = -p.x, y=-p.y}
else {x = p.x, y=p.y}
let d = distMoved(flipIfGreen, {x=3.0, y=4.0})
The other way works!
let distMoved (f : {x:float,y:float}->{x:float,y:float},
p : {x:float,y:float}) =
let p2 : {x:float,y:float} = f p in
let dx : float = p2.x – p.x in
let dy : float = p2.y – p.y in
Float.sqrt(dx*dx + dy*dy)
let flipX_Y0 p = {x = -p.x, y=0.0}
let d = distMoved(flipX_Y0, {x=3.0, y=4.0})
Can do both
{x:float} -> {x:float,y:float,color:string}
{x:float,y:float} -> {x:float,y:float}
let distMoved (f : {x:float,y:float}->{x:float,y:float},
p : {x:float,y:float}) =
let p2 : {x:float,y:float} = f p in
let dx : float = p2.x – p.x in
let dy : float = p2.y – p.y in
Float.sqrt(dx*dx + dy*dy)
let flipXMakeGreen p = {x = -p.x, y=0.0, color="green"}
let d = distMoved(flipXMakeGreen, {x=3.0, y=4.0})
Conclusion