TAPL Subtyping

Presented by Pinglei Guo for UCSC CMPS253 on 11/07/2017

Table of content

NOTE: the title and order of some sections are modfied to emphasize specific content

Notation

We use a more Java-style pseudo code instead of lambda-calculus to have named function and multi arguments

The style we should use

(* NOTE: from [2], rewrite in a mix of OCaml and lambda-calculus, won't compile *)
let sphere = {center={x=1, y=2, z=3}, radius=2} in
    let circle = sphere in 
        (lambda c:{center: {x: Nat, y:Nat}, radius: Nat}. c.center={x=0, y=0}) circle
    sphere.center.z

The style we actually use (similar to [2])

/* {center: {x:Nat, y:Nat}, radius: Nat} -> Unit */
function move_to_origin(c: {center: {x:Nat, y:Nat}, radius: Nat}) : Unit {
    c.center = {x=0, y=0}
}
var sphere = {center={x=1, y=2, z=3}, radius=2}
move_to_origin(sphere)
sphere.center.z

15 Subtyping

15.1 Motivation

Recall the typing rule for function application

$$\frac{\Gamma \vdash t_1 : T_{11} \rightarrow T_{12} \quad \Gamma \vdash t_2 : T_{11}}{\Gamma \vdash t_1 \ t_2 : T_{12}} \quad\quad \textbf{(T-APP)}$$

This is a well typed term

$$(\lambda \text{r:{x:Nat}. r.x) {x=0}}$$

This is not typable, but it looks reasonable

$$(\lambda \text{r:{x:Nat}. r.x) {x=0,y=1}}$$

However, passing {x=0,y=1} to ($\lambda$ r:{x:Nat}. r.x) should yield 0, the function only needs field x and don't care other fields in record. So we extend our rule to allow this kind of operation, where a more informative type ({x:Nat, y:Nat}) is used in the place of a less informative one ({x:Nat}).

We can use {x=0, y=1} in palace of {x=0}, we say {x:Nat, y:Nat} is a subtype, written <: of {x:Nat}. It's sub because the number of records of the subtype is less. [3]

Foramally, S is a subtype of T written S <: T, mean any term of type S can safely be used in a context where a term of type T is expected

$$\frac{\Gamma \vdash t : S \quad S <: T}{\Gamma \vdash t : T} \quad\quad \textbf{(T-SUB)}$$

Intuitively, our subtype relation also have reflexivity and transitivity

$$ S <: S \quad \textbf{(S-REFL)}$$$$\frac{S <:U \quad U <: T}{S <: T} \quad\quad \textbf{(S-TRANS)}$$

Question

Anything strange about our T-SUB rule compared w/ other typing rules when syntax is considered?

Syntax

t :: = x               variable
     lambda x:T. t     abstraction (function)
     t t               application (function call)

Typing rules

$$\frac{x:T \in \Gamma}{\Gamma \vdash x : T} \quad\quad \textbf{(T-VAR)}$$$$\frac{\Gamma,x:T_1 \vdash t_2:T_2}{\Gamma \vdash \lambda x:T_1. t_2:T_1 \rightarrow T_2} \quad\quad \textbf{(T-ABS)}$$$$\frac{\Gamma \vdash t_1 : T_{11} \rightarrow T_{12} \quad \Gamma \vdash t_2 : T_{11}}{\Gamma \vdash t_1 \ t_2 : T_{12}} \quad\quad \textbf{(T-APP)}$$$$\frac{\Gamma \vdash t : S \quad S <: T}{\Gamma \vdash t : T} \quad\quad \textbf{(T-SUB)}$$

This will be answered in Chapter 16

15.2 Record

We formalize {x=0, y=1} <: {x=0} as width subtyping rule

$$\{l_i:T_i^{i \in 1..n+k}\} <: \{l_i:T_i^{i \in 1..n}\} \quad \textbf{(S-RCDWIDTH)}$$

It might looks counter intuitive that the "smaller" type (subtype) actually has more fields. That is because more fields in the record type means more restriction and a smaller set it can cover, the following euler diargam shows this relation, {x:Nat, y:Nat, z:Nat} <: {x:Nat, y:Nat} <: {x:Nat} and {x:Nat, y:Nat} <: {y:Nat}

The "smaller" record type has more fields because there are less records of this type

Question

If we specialize record to tuple, how many circle would left in above diagram?

In S-RcdWidth, we require each field has identitical type, however, this restriction can be loosen, they can have subtype relation, this gives us the depth subtyping rule

$$\frac{\text{for each } i \quad S_i <: T_i}{\{l_i:S_i^{i \in 1..n}\} <: \{l_i:T_i^{i \in 1..n}\}} \quad\quad \textbf{(S-RCDDEPTH)}$$

We can combine S-RcdWidth and S-RcdDepth together

$$\frac{ \text{\{ x: Nat, y: Nat, z: Nat\}} <: \text{\{x: Nat, y: Nat\}} \quad \text{\{radius: Nat\}} <: \text{\{radius: Nat\}} } {\text{\{center: {x: Nat, y: Nat, z: Nat}, radius: Nat\}} <: \text{\{center: {x: Nat, y: Nat}, radius: Nat\}} }$$

Question

If we allow mutable record, what would happen? [2]

/* {center: {x:Nat, y:Nat}, radius: Nat} -> Unit */
function move_to_origin(c: {center: {x:Nat, y:Nat}, radius: Nat}) : Unit {
    c.center = {x=0, y=0}
}
var sphere = {center={x=1, y=2, z=3}, radius=2}
move_to_origin(sphere)
sphere.center.z // 3?

This will be answered in 15.5

The order of fields in record type does not have impact on safety because our projection is insensitive to it, so we have the last rule for record S-RcdPerm

$$\{l_i:T_i^{i \in 1..n+k}\} <: \{l_i:T_i^{i \in 1..n}\} \quad \textbf{(S-RCDWIDTH)}$$$$\frac{\text{for each } i \quad S_i <: T_i}{\{l_i:S_i^{i \in 1..n}\} <: \{l_i:T_i^{i \in 1..n}\}} \quad\quad \textbf{(S-RCDDEPTH)}$$$$\frac{\{k_j:S_j^{j\in 1..n}\} \text{ is a permuatation of } \{l_i:T_i^{i\in 1..n}\}} {\{k_j:S_j^{j\in 1..n}\} <: \{l_i:T_i^{i\in 1..n}\}} \quad\quad \textbf{(S-RCDPERM)}$$

In fact, record subtyping is not widely adopted, OCaml is using row-variable polymorphism for records [1].

15.5 Variants

Variants is similar to record in some sense, record is a combination of many types, variant is also a combination of many types, but it can only be one of them at given time. Intuitively we have depth and permutation rule for variants.

$$\frac{\text{for each } i \quad S_i <: T_i}{\{l_i:S_i^{i \in 1..n}\} <: \{l_i:T_i^{i \in 1..n}\}} \quad\quad \textbf{(S-RCDDEPTH)}$$$$\frac{\text{for each } i \quad S_i <: T_i}{\langle l_i:S_i^{i \in 1..n}\rangle <: \langle l_i:T_i^{i \in 1..n}\rangle} \quad\quad \textbf{(S-VARIANTDEPTH)}$$$$\frac{\{k_j:S_j^{j\in 1..n}\} \text{ is a permuatation of } \{l_i:T_i^{i\in 1..n}\}} {\{k_j:S_j^{j\in 1..n}\} <: \{l_i:T_i^{i\in 1..n}\}} \quad\quad \textbf{(S-RCDPERM)}$$$$\frac{\langle k_j:S_j^{j\in 1..n}\rangle \text{ is a permuatation of } \langle l_i:T_i^{i\in 1..n}\rangle} {\langle k_j:S_j^{j\in 1..n}\rangle <: \langle l_i:T_i^{i\in 1..n}\rangle} \quad \textbf{(S-VARIANTPERM)}$$

But what about width rule?

$$\{l_i:T_i^{i \in 1..n+k}\} <: \{l_i:T_i^{i \in 1..n}\} \quad \textbf{(S-RCDWIDTH)}$$
$$\langle l_i:T_i^{i \in 1..n}\rangle <: \langle l_i:T_i^{i \in 1..n+k}\rangle \quad \textbf{(S-VARIANTWIDTH)}$$

Why the opposite in record and variants?

  • record: more fields means more requirement, less choice, thus subtype
  • variants: more fields means more choice, thus supertype

15.2 S-Arrow

In a high order language, function can be argument to other function. Recall our subtyping rule, t does not have to be a variable term.

$$\frac{\Gamma \vdash t : S \quad S <: T}{\Gamma \vdash t : T} \quad\quad \textbf{(T-SUB)}$$

The text book says we should use this one for function type

$$\frac{T_1 <: S_1 \quad S_2 <: T_2}{S_1 \rightarrow S_2 <: T_1 \rightarrow T_2} \quad\quad (\textbf{S-ARROW})$$

But there are total 4 combination, what about the other 3?

$$\frac{S_1 <: T_1 \quad S_2 <: T_2}{S_1 \rightarrow S_2 <: T_1 \rightarrow T_2} \quad\quad (\textbf{S-ARROW-1})$$$$\frac{T_1 <: S_1 \quad T_2 <: S_2}{S_1 \rightarrow S_2 <: T_1 \rightarrow T_2} \quad\quad (\textbf{S-ARROW-2})$$$$\frac{S_1 <: T_1 \quad T_2 <: S_2}{S_1 \rightarrow S_2 <: T_1 \rightarrow T_2} \quad\quad (\textbf{S-ARROW-3})$$

Note on the left side of $\rightarrow$, the relation is contravariant while on the right side, the relation is covariant, which is pretty counter intuitive. One might expect the left side (arugment) is convariant. Possible source of confusion are:

  • in previous example of function application, we replace argument with its subtype. i.e. replace {x:Nat} with {x:Nat, y:Nat}
  • only think about the argument of the function, NOT taking function as a whole. [5]

Let's look at a concrete example before we dive into this rule.

Assume we have a game called Game of the Type, you are the main character John Calculus, you want to travel to the north safely, you can use the following transportation for one time and walk all the rest

  • a dragon rent from Nights King, can fly or crawl on the ground if you like
  • a train called ruby, can only move veritically on rails

Following is the Pseudo-Code, the implementation is written in python and use dict to represent record.

/* 
* Game of the Type - The Journey of John Calculus
*/
var start = {x:0, y:0} // your home
var end = {x:101, y:505} // the wall

function train(p:{y:Nat}) : {y:Nat} {
    return {y=p.y+50}
}

function walk(p:{x:Nat, y:Nat}) : {x:Nat, y:Nat} {
    return {x=p.x+1, y=p.y+5}
}

function dragon(p:{x:Nat, y:Nat, z:Nat}) : {x:Nat, y:Nat, z:Nat} {
    return {x=p.x+10, y=p.y+50, z=p.z+1}
}

function travel(transport:{x:Nat, y:Nat} -> {x:Nat, y:Nat}, s:{x:Nat, y:Nat}) : Unit {
    p := transport(s)
    p := walk(p)
    print(p)
}

Case-0, walk all the way (exact same type)

$$S_1 = T_1 = \text{\{x:Nat, y:Nat\}}= S_2 = T_2$$

In [4]:
# {x:Nat, y:Nat} -> {x:Nat, y:Nat}
def walk(p):
    return {'x': p['x']+1, 'y': p['y']+5}

# ({x:Nat, y:Nat} -> {x:Nat, y:Nat}) -> {x:Nat, y:Nat} -> Unit
def travel(transport, s):
    p = transport(s)
    p = walk(p)
    print('{} and walk, now I am at {}'.format(transport.__name__, p))
    
travel(walk, {'x': 0, 'y': 0})


walk and walk, now I am at {'x': 2, 'y': 10}

Case-1, dragon, covariant for both argument and return value

$$S_1 = S_2 = \text{\{x:Nat, y:Nat, z:Nat\}} <: \text{\{x:Nat, y:Nat}\} = T_1 = T_2$$$$\frac{S_1 <: T_1 \quad S_2 <: T_2}{S_1 \rightarrow S_2 <: T_1 \rightarrow T_2} \quad\quad (\textbf{S-ARROW-1})$$

In [5]:
# {x:Nat, y:Nat, z:Nat} -> {x:Nat, y:Nat, z:Nat}
def dragon(p):
    return {'x': p['x']+10, 'y': p['y']+50, 'z': p['z']+1}

In [6]:
travel(dragon, {'x': 0, 'y': 0})


---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-6-237ea11322ea> in <module>()
----> 1 travel(dragon, {'x': 0, 'y': 0})

<ipython-input-4-c4c206398a2b> in travel(transport, s)
      5 # ({x:Nat, y:Nat} -> {x:Nat, y:Nat}) -> {x:Nat, y:Nat} -> Unit
      6 def travel(transport, s):
----> 7     p = transport(s)
      8     p = walk(p)
      9     print('{} and walk, now I am at {}'.format(transport.__name__, p))

<ipython-input-5-d5e24f1d32d0> in dragon(p)
      1 # {x:Nat, y:Nat, z:Nat} -> {x:Nat, y:Nat, z:Nat}
      2 def dragon(p):
----> 3     return {'x': p['x']+10, 'y': p['y']+50, 'z': p['z']+1}

KeyError: 'z'

In fact, covariant for both argument and return value sometimes does work


In [7]:
start = {'x': 0, 'y': 0}
# {x:Nat, y:Nat, z:Nat} <: {x:Nat, y:Nat}
new_start = {'x': start['x'], 'y': start['y'], 'z': 0}
travel(dragon, new_start)


dragon and walk, now I am at {'x': 11, 'y': 55}

But our type system need to be sound, not might work when special input is provided

Case-2, train, contravariant for both argument and return value

$$T_1 = T_2 = \text{\{x:Nat, y:Nat\}} <: \text{\{y:Nat}\} = S_1 = S_2$$$$\frac{T_1 <: S_1 \quad T_2 <: S_2}{S_1 \rightarrow S_2 <: T_1 \rightarrow T_2} \quad\quad (\textbf{S-ARROW-2})$$

In [8]:
# {y:Nat} -> {y:Nat}
def train(p):
    return {'y': p['y']+50}

In [9]:
travel(train, {'x': 0, 'y': 0})


---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-9-bd7fd8dc9438> in <module>()
----> 1 travel(train, {'x': 0, 'y': 0})

<ipython-input-4-c4c206398a2b> in travel(transport, s)
      6 def travel(transport, s):
      7     p = transport(s)
----> 8     p = walk(p)
      9     print('{} and walk, now I am at {}'.format(transport.__name__, p))
     10 

<ipython-input-4-c4c206398a2b> in walk(p)
      1 # {x:Nat, y:Nat} -> {x:Nat, y:Nat}
      2 def walk(p):
----> 3     return {'x': p['x']+1, 'y': p['y']+5}
      4 
      5 # ({x:Nat, y:Nat} -> {x:Nat, y:Nat}) -> {x:Nat, y:Nat} -> Unit

KeyError: 'x'

Case-3, dragon_train, covariant for argument and contravariant for return value

$$ \text{\{x:Nat, y:Nat, z:Nat\}} = S_1 <: T_1 = \text{\{x:Nat, y:Nat\}} = T_2 <: S_2 = \text{\{y:Nat\}}$$$$\frac{S_1 <: T_1 \quad T_2 <: S_2}{S_1 \rightarrow S_2 <: T_1 \rightarrow T_2} \quad\quad (\textbf{S-ARROW-3})$$

In [11]:
# {x:Nat, y:Nat, z:Nat} -> {y:Nat}
def dragon_train(p):
    p = {'x': p['x'], 'y': p['y'], 'z': 0}
    return train(dragon(p))

In [12]:
travel(dragon_train, {'x': 0, 'y': 0})


---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-12-f9e74fb0b4f9> in <module>()
----> 1 travel(dragon_train, {'x': 0, 'y': 0})

<ipython-input-4-c4c206398a2b> in travel(transport, s)
      6 def travel(transport, s):
      7     p = transport(s)
----> 8     p = walk(p)
      9     print('{} and walk, now I am at {}'.format(transport.__name__, p))
     10 

<ipython-input-4-c4c206398a2b> in walk(p)
      1 # {x:Nat, y:Nat} -> {x:Nat, y:Nat}
      2 def walk(p):
----> 3     return {'x': p['x']+1, 'y': p['y']+5}
      4 
      5 # ({x:Nat, y:Nat} -> {x:Nat, y:Nat}) -> {x:Nat, y:Nat} -> Unit

KeyError: 'x'

Case-4, train_dragon, contravariant for argument and covariant for return value

$$ \text{\{x:Nat, y:Nat, z:Nat\}} = S_2 <: T_2 = \text{\{x:Nat, y:Nat\}} = T_1 <: S_1 = \text{\{y:Nat\}}$$$$\frac{T_1 <: S_1 \quad S_2 <: T_2}{S_1 \rightarrow S_2 <: T_1 \rightarrow T_2} \quad\quad (\textbf{S-ARROW})$$

In [13]:
# {y:Nat} -> {x:Nat, y:Nat, z:Nat}
def train_dragon(p):
    x = p['x']
    p = train(p)
    p = {'x': x, 'y': p['y'], 'z': 0} # get off the train and get on the dragon
    return dragon(p)

In [14]:
travel(train_dragon, {'x': 0, 'y': 0})


train_dragon and walk, now I am at {'x': 11, 'y': 105}

In conclusion, you can replace a function w/ its subtype because the subtype

  • has less requirement for argument (loosen input, contravariant) thus won't break upstream (function that call this function)
  • is more specific for output (strict ouput, covariant) thus won't break down stream (function that accept return value of this function)
$$\text{loosen input, strict output (both ok) } \frac{T_1 <: S_1 \quad S_2 <: T_2}{S_1 \rightarrow S_2 <: T_1 \rightarrow T_2} \quad\quad (\textbf{S-ARROW})$$$$\text{strict input (upstream fail), strict output } \frac{S_1 <: T_1 \quad S_2 <: T_2}{S_1 \rightarrow S_2 <: T_1 \rightarrow T_2} \quad\quad (\textbf{S-ARROW-1})$$$$\text{loosen input, loosen output (downstream fail) } \frac{T_1 <: S_1 \quad T_2 <: S_2}{S_1 \rightarrow S_2 <: T_1 \rightarrow T_2} \quad\quad (\textbf{S-ARROW-2})$$$$\text{strict input, loosen output (both fail) } \frac{S_1 <: T_1 \quad T_2 <: S_2}{S_1 \rightarrow S_2 <: T_1 \rightarrow T_2} \quad\quad (\textbf{S-ARROW-3})$$

Visualize function subtyping

If we treat functions as funnel [5] and value is water flow inside them from top to bottom, safe means not leaking water.

Case-0 exact same type $S_1 = T_1$ and $S_2 = T_2$

Case-1 covariant in both argument and return value $S_1 <: T_1$ and $S_2 <: T_2$

Case-2 contravariant in both argument and return value $T_1 <: S_1$ and $T_2 <: S_2$

Case-3 covariant in argument and contravariant in return value $S_1 <: T_1$ and $T_2 <: S_2$

S-Arrow, contravariant in argument and covariant in return value $T_1 <: S_1$ and $S_2 <: T_2$

15.4 Top & Bottom

  • top: a supertype of every type S <: Top
  • bottom: a subtype of every type Bot <: T

Top

  • like Object in Java

Bot

  • is empty, no closed values
  • not like null in Java [5]
  • too complex for typecheck, omit in rest of the book

15.5 Cast, Reference

Up-Cast

  • hide some detail
{x=0, y=1} as {x:Nat} (* hide y *)
public void UpCast(HashSet<String> hs) {
    Set<String> s = hs;
}

Down-Cost

  • not sound
  • "poor man"'s polymorphism (Java)
  • use type tag
$$f = \lambda\text{x:Top. (x as \{a:Nat\}).a};$$
public void DownCast(Set<String> s) {
    HashSet<String> hs = (HashSet<String>) s;
}
$$\frac{\Gamma \vdash t_1:S}{\Gamma \vdash t_1 \text{ as } T : T} \quad\quad \textbf{T-DOWNCAST}$$$$\frac{\vdash v_1 : T}{v_1 \text{ as } T \rightarrow v_1} \quad\quad \textbf{E-DOWNCAST}$$

16 Metatheory of Subtyping

  • the declarative rules are not syntax directed, $t$ can be any term
$$\frac{\Gamma \vdash t : S \quad S <: T}{\Gamma \vdash t : T} \quad\quad \textbf{(T-SUB)}$$
  • S-REFL always works
$$ S <: S \quad \textbf{(S-REFL)}$$
  • need to guess U
$$\frac{S <:U \quad U <: T}{S <: T} \quad\quad \textbf{(S-TRANS)}$$

We need a more practical (algorithmic) rule that is equivalent to our current declarative subtyping rule

16.1 Algorithmic Subtyping

  • merge record subtyping rule, which use S-TRANS and S-REFL to drop S-TRANS and S-REFL
$$\frac{\{l_i^{i \in 1..n}\} \subseteq \{k_j^{j \in 1..m}\} \quad k_j = l_j \text{ implies } S_j <: T_i} {\{k_j:S_j^{j \in 1..m}\} <: \{l_i:T_i^{i \in 1..n}\}} \quad \textbf{S-RCD}$$

Thus we got the algorithmic subtyping after we have proved its relationship w/ the declarative one

(no more latex, just compy and paste)

Based on this algorithmic rule, we can write a pseudo implementation

function subtype(S:Ty, T:Ty) : Bool {
    // anytype is subtype of Top
    if T == Top then 
        true
    // S-Arrow
    else if S = S1 -> S2 && T = T1 -> T2 then
        // contravariant in argument and covariant in result
        subtype(T1, S1) && subtype(S2, T2)
    // Record
    else if S is record && T is record then
        for l in S.fields()
            T.contains(l) && subtype(S[l], T[l])
    // Variants
    else if S is variant && T is variant then
        for l in T.fields()
            S.contains(l) && subtype(S[l], T[l])
    else 
        false
}

Question Anything wrong in our pseudo code?

16.2 Algorithmic Typing

Take away

  • function subtyping is contravariant for argument and covariant for return value
  • record w/ more fields is subtype while variants w/ fields is supertype
  • reference (mutable) makes subtyping complex
  • algorithmic subtyping and typing are introduced for practical implementation and is equivalent w/ declarative one
  • it's Week 6 (or 7) already
  • the source of the presentation is on GitHub at15/papers-i-read
  • implementation is currently on GitHub at15/reika