Personal Blog by Galen Wong

My Notes for UCLA CS231: Types and Programming Languages

Winter 2021, taught by professor Todd Millstein

2021-08-29107 min read

This is my notes for CS231 Types and Programming Languages during Winter 2021 at UCLA. I find this class extremely interesting and want to share my notes for people are/will be taking the course as a reference, and for those considering taking the course to see what the content is like. If you are an UCLA student considering taking this course, I am happy to answer any questions and talk about the class with you.

Quick FAQ:

  • What is the course about?
    • This course is about the theory of type checking. The core goal of the class is to show you how type checking guarantees runtime “correctness”. In the course, you will get to construct an OCaml-like language from ground up by adding features (variables, functions, tuples, unions) one at a time. While adding each feature, we use maths to prove that our type system will guarantee that the feature will run correctly at runtime. Towards the end of the course, we get to use an automatic theorem prover called Coq.
  • How are the assignments and exams?
    • Since the course is about proving that type checking works, you will be writing some proofs in the homework. You will also write some OCaml code. (Don’t be scared, the OCaml code assignment is not like 131.) The exam is very similar style to the homework. If you can handle the homework, you can handle the exam.
  • If I am an undergrad, can I take this course?
    • Yes! I took this course as an undergrad. Check with the department for the process to enroll in grad classes as undergrad.
  • What are some background knowledge that helped in 231?
    • The course enforces CS 131 as a pre-requisite. You need to know some basic OCaml for the coding homework, and having some basic understanding of the OCaml type constructs like tuple, unions, pattern matching, and functions definitely helps. I also took CS 132 (compiler construction) before 231, which helped me gain some experience of writing code for type checking. But I will say 132 is nice to have but not necessary. Knowing how to write LaTeX helped with the homework.

Table of Contents for Notes:

Operational Semantics

Big-step operational semantics

The simple language that we are working with today defined in BNF grammar:

t ::= true | false | if t then t else t

The language gives some results. We call these values and they can be defined as grammar as well.

v ::= true | false

P.S. for this class. Values are usually a subset of the grammar.

We first define a interpreter for this language.

(* the types to represent the tree *)
type t = True | False | If of t * t * t
(* 
 * How to initialize If:
 *   If (True, False, True)
 *)

(* the values/outputs of the program *)
type v = TrueV | FalseV

An interpreter at this point is a function that takes a term of type t and return a value of type v.

let rec eval (t:t):v = 
    match t with 
        True -> TrueV
    |   False -> FalseV
    |   If(cond, thn, els) ->
        match eval cond with
            TrueV -> eval thn
        |   FalseV -> eval els

So, big-step operation semantics really means we define a function that takes in a term to produce a value. This relationship is the big step operation semantics. Notation: tvt \Downarrow v (read as tt big-steps to vv)

In world of logic: binary relation, predicate, logical judgement.

Big-step operational semantics give rules to define the relation. Notation of a rule:

PremisesConclusion\cfrac{\text{Premises}}{\text{Conclusion}}

We attempt to writes the rules for the simple language defined above.

vvEval\cfrac{}{v \Downarrow v}\quad E_{val}\quad

The rule EvalE_{val} has no premises. It is called an axiom. The logical equivalence of this rule is:

v.vv\forall v. \quad v \Downarrow v

Now we consider a rule where If condition evals to true.

t1truet2v2If t1 then t2 else t3v2EIfTrue\cfrac{t_1 \Downarrow true\quad t_2 \Downarrow v_2} { \texttt{If t1 then t2 else t3} \Downarrow v_2 } \quad E_{IfTrue}

Logical equivalence

t1,t2,t3,v2.(t1truet2v2)If t1 then t2 else t3v2\forall t_1,t_2,t_3,v_2. \\ (t_1 \Downarrow true \land t_2 \Downarrow v_2) \Rightarrow \texttt{If t1 then t2 else t3} \Downarrow v_2

We can define the rule for false.

t1falset3v3If t1 then t2 else t3v3EIfFalse\cfrac{t_1 \Downarrow false\quad t_3 \Downarrow v_3} { \texttt{If t1 then t2 else t3} \Downarrow v_3 } \quad E_{IfFalse}

Applying inference rule

Claim: if true then false else true \Downarrow false

We pick a rule to apply to the claim. We appeal to EIfTrueE_{IfTrue}. Then we recursively prove each preconditions.

truetrueEvalfalsefalseEvalif true then false else truefalseEIfTrue\cfrac{ \cfrac{}{true \Downarrow true}E_{val} \quad \cfrac{}{false \Downarrow false}E_{val} } { \texttt{if true then false else true} \Downarrow false } \quad E_{IfTrue}

This is a derivation tree. To know the tree is correct. We can simply apply the rules and see if the correct predicates are evaluated. So a simple process.

If we choose the wrong rule initially. Our proof cannot proceed. A derivation attempt that fails:

truefalsetruefalseif true then false else truefalseEIfFalse\cfrac{ \cfrac{}{true \Downarrow false} \quad \cfrac{}{true \Downarrow false} } { \texttt{if true then false else true} \Downarrow false } \quad \boldsymbol{E_{IfFalse}}

Goal: if true then (if false then true else false) else true \Downarrow false

truetrueEvalfalsefalseEvalfalsefalseEvalif false then true else falsefalseEIfFalseif true then (if false then true else false) else truefalseEIfTrue\cfrac{ \cfrac{}{true \Downarrow true}E_{val} \quad \cfrac{ \cfrac{}{false \Downarrow false} E_{val} \quad \cfrac{}{false \Downarrow false} E_{val} }{\texttt{if false then true else false}\Downarrow false}E_{IfFalse} } { \texttt{if true then (if false then true else false) else true} \Downarrow false } \quad E_{IfTrue}

Now we make our language more interesting by adding numbers.

t ::= true | false | if t then t else t 
     | n | t + t | t > t
n ::= <integer constants>

We manipulate the interpreter.

type t = True | False | If of t * t * t
        | Int of int | Plus of t * t | Greater of t * t

type v = TrueV | FalseV | IntV of int

let rec eval (t:t):v = 
    match t with 
        True -> TrueV

    |   False -> FalseV

    |   If(cond, thn, els) ->
        match eval cond with
            TrueV -> eval thn
        |   FalseV -> eval els

    |   Int i -> IntV i

    |   Plus (t1, t2) -> 
        (match (eval t1, eval t2) with -> 
            (Intv i, IntV j) -> IntV (i+j))

    |   Greater(t1, t2) -> 
        (match (eval t1, eval t2) with -> 
            (IntV i, IntV j) -> 
                if i > j then TrueV else FalseV)

When Plus evals does not match two ints, OCaml throws a runtime error. To

be nicer, we add a Exception match clause

_ -> raise TypeError

This is dynamically typed language. If we don’t encounter type error during execution, we don’t do anything. For instance, an if-expression:

if true then true else false + 1

since the else-branch is never evaluated, we don’t care about the type mismatch there. We continue from last time. Let’s write out the rule for Plus and Greater than.

Plus

t1n1t2n2n1[[+]]n2=nt1+t2nEPlus\cfrac{ t_1 \Downarrow n_1 \quad t_2 \Downarrow n_2 \quad n_1 [[+]] n_2 = n }{ t_1 + t_2 \Downarrow n } \quad E_{Plus}
  • Note that [[+]][[+]] means a mathematical plus

Greater

t1n1t2n2n1[[>]]n2=bt1+t2bEGreater\cfrac{ t_1 \Downarrow n_1 \quad t_2 \Downarrow n_2 \quad n_1 [[>]] n_2 = b }{ t_1 + t_2 \Downarrow b } \quad E_{Greater}

We introduce another metavariable for type of value for boolean.

t ::= b | if t then t else t
    | n | t + t | t > t
b ::= true | false
n ::= integer constants
v ::= b | n

Proving properties given rules

Let’s go back to the simplistic OCaml with only boolean without the integer values.

Theorem: For all terms t, there exists value v such that t \Downarrow v.

Note this theorem might not be true for turing complete languages. The term might get stuck in infinite loop.

There is infinite number of terms. We use induction to prove it.

Structural induction

We want to prove P(t)P(t) for all terms tt.

  • Prove P(t)P(t) for separately for each kind of term
    • we can assume that P(t)P(t') holds for each subterm tt' in tt

Proof: by structural induction on tt.

  • Induction hypothesis: for all terms tt' such that tt' is a subterm of tt,

there exists value vv' such that tvt' \Downarrow v'.

  • Case analysis on the root node in tt.
    • Case tt=true: By EvalE_{val}, truetruetrue \Downarrow true.
    • Case tt=false: By EvalE_{val}, falsefalsefalse \Downarrow false.
    • Case tt = if t1 then t2 else t3:
      • By induction hypothesis on t1t_1, there exists v1v_1 such that t1t_1 \Downarrow v1v_1.
        • Case v1v_1=true: By IH on t2t_2, there exists v2v_2 such that t2v2t_2 \Downarrow v_2.
          • Then by EIfTrueE_{IfTrue} if t1 then t2 else t3 v2\Downarrow v_2
        • Case v1v_1=false: By IH on t2t_2, there exists v3v_3 such that t3v3t_3 \Downarrow v_3.
          • Then by EIfFalseE_{IfFalse} if t1 then t2 else t3 v3\Downarrow v_3
  • QED

You can only do a case analysis on something that you know to exists/be true.

Example: for the same theorem, we cannot do a case analysis on vv. Since the goal is to prove vv exists, to do a case analysis we are just saying that vv exists.

Small-step operation semantics

Judgement of form ttt\rightarrow t', meaning tt evaluates in one step to tt'. It is a single step of derivation. What is a single step of derivation?

Example:

if (if true then false else true) then false else true 

(* single-step to --> *)

if false then false else true 

(* single-step to --> *)

true

Two types of small-step rules:

  • “computation” rules that simply do one small bit of evaluation
  • “congruence” rules that simply tell you where to look recursively for the next bit of work

Examples of computation rule:

if true then t2 else t3t2EIfTrue\cfrac{}{ \texttt{if true then t2 else t3} \rightarrow t2 } \quad E_{IfTrue}
if false then t2 else t3t3EIfFalse\cfrac{}{ \texttt{if false then t2 else t3} \rightarrow t3 } \quad E_{IfFalse}

Well, not all if statements simply uses true or false as the condition. We need a congruence rule to tell us what to do

t1t1if t1 then t2 else t3if t1’ then t2 else t3EIf\cfrac{ t1 \rightarrow t1' }{ \texttt{if t1 then t2 else t3} \rightarrow \texttt{if t1' then t2 else t3} } \quad E_{If}
  • from this congruence rule, we ended up with a new piece of program.
  • a derivation tree here is simply a derivation list.

An example of derivation:

 if true then false else true falseEIfTrue if (if true then false else true) then false else true  if false then false else true EIf\cfrac{ \cfrac{}{ \texttt{ if true then false else true } \rightarrow \texttt{false} } E_{IfTrue} }{ \begin{array}{l} \texttt{ if (if true then false else true) then false else true } \rightarrow\\ \texttt{ if false then false else true } \end{array} } E_{If}

Plus rules

n1[[+]]n2=nn1+n2nEPlusRed\cfrac{ n1 [[+]] n2 = n }{ n1 + n2 \rightarrow n }\quad E_{PlusRed}
  • Red stands for reduction
t1t1t1+t2t1+t2EPlus1\cfrac{t1 \rightarrow t1'}{ t1 + t2 \rightarrow t1' + t2 }\quad E_{Plus1}
  • Notice that we have taken explicit control over the order of evaluation: always left term first
t2t2v1+t2v1+t2EPlus2\cfrac{t2 \rightarrow t2'}{ v1 + t2 \rightarrow v1 + t2' }\quad E_{Plus2}
  • notice that we didn’t enforce the type of v1v1 since, we only enforce the

requirement at the moment of addition (aka EPlusRedE_{PlusRed}). This is nice since the operands can be different types and it is useful to put constraints only when it is absolutely needed.

A term tt is called a normal form if there does not exist a tt' such that ttt \rightarrow t'.

  • example: true
  • in general, we will arrange it such that values are normal forms

There are normal forms that are not values.

  • example: true + 5

A normal form that is not a value is called a stuck expression

  • run-time type error

Stuck expression can be arbitrarily large.

  • That means you cannot take a step to get a smaller program
  • (true) + 5 is stuck
  • if (true + 5) then 0 else 1 is stuck
  • if (if (true + 5) then 0 else 1) then 0 else 1 is stuck

Define a second judgement that is simply reflexive, transitive closure of the \rightarrow relation. “Multi-step relation” notated as ttt\rightarrow^* t'.

ttMRefl\cfrac{}{t \rightarrow^* t} \quad M_{Refl}
ttttMStep\cfrac{t \rightarrow t'}{t\rightarrow^* t'} \quad M_{Step}
ttttttMTransitive\cfrac{t \rightarrow^* t''\quad t'' \rightarrow^*t'}{t\rightarrow^* t'} \quad M_{Transitive}

Now we can derive the execution of the entire program using this relation in a single tree just as in big-step semantics.

Definition: a term is eventually stuck if ttt \rightarrow^* t' and tt' is stuck.

(true + 5) is eventually stuck

  • in general, all stuck expressions are also eventually stuck.

Example of eventually stuck: if true then (5 + true) else 0

  • note that this expression is not stuck. We can still take a step to reduce it to 5 + true
    • if true then (5 + true) else 0 \rightarrow 5 + true
  • note that if (5 + true) then 1 else 0 is stuck tho. Since we cannot take an step to reduce it

What is this in a real programming language?

  • a bad program
  • a program that will eventually crash

Practicing Proof

Define the falseCountfalseCount of a term tt as follows:

  • falseCount(true)=0falseCount(true) = 0
  • falseCount(false)=1falseCount(false) = 1
  • falseCount(if t1 then t2 else t3)=falseCount(t1)+falseCount(t2)+falseCount(t3)falseCount(\text{if t1 then t2 else t3}) = falseCount(t1) + falseCount(t2) + falseCount(t3)

We are given a lemma FCNonnegFC_{Nonneg}: for all tt, falseCount(t)0falseCount(t) \geq 0.

Theorem: If ttt \longrightarrow t', then falseCount(t)falseCount(t)falseCount(t') \leq falseCount(t).

Proof: we perform a structural induction on tt.

  • induction hypothesis: For all subterm tt' of tt, if ttt' \rightarrow t'', falseCount(t)falseCount(t)falseCount(t'') \leq falseCount(t')

We can do case analysis on root node term tt. An alternative, when we are given a derivation (ttt\rightarrow t'), is to do case analysis on the rules.

Case analysis on the root rule in the derivation of ttt\rightarrow t':

  • Case EIfTrueE_{IfTrue}: Then t=if true then t2 else t3 t = \texttt{if true then t2 else t3 } and $t’ =

t2$.

  • By definition of falseCount(t)=falseCount(t2)+falseCount(t3)falseCount(t)= falseCount(t2) + falseCount(t3).
  • By FCNonnegFC_{Nonneg}, falseCount(t2)0falseCount(t2) \geq 0 and falseCount(t3)0falseCount(t3) \geq 0.
  • Therefore, falseCount(t2)+falseCount(t3)falseCount(t2)falseCount(t2) + falseCount(t3) \geq falseCount(t2)
  • Case EIfFalseE_{IfFalse}, same idea as above
  • Case EIfE_{If}, Then t=if t1 then t2 else t3t=\texttt{if t1 then t2 else t3} and t=if t1’ then t2 else t3t'=\texttt{if t1' then t2 else t3} and t1t1t1\rightarrow t1'.
    • By IH, falseCount(t1)falseCount(t1)falseCount(t1') \leq falseCount(t1)
    • Therefore, falseCount(t1)+falseCount(t2)+falseCount(t3)falseCount(t1)+falseCount(t2)+falseCount(t3)falseCount(t1') + falseCount(t2) + falseCount(t3) \leq falseCount(t1) + falseCount(t2) + falseCount(t3).
    • THen, the result follows from the definition of falseCountfalseCount.

If we were to perform a case analysis, we also have to cover cases where term is true and we need to say that there is no derivation possible by deriving a contradiction, very annoying.

Note: this theorem does not hold when we add integer into the language. Since we can derive false from 0 > 1.

Exercise: proving lemma FCNonnegFC_{Nonneg}:

  • By structural induction on tt.
  • IH: for all tt' which is a subterm of tt, falseCount(t)0falseCount(t') \geq 0
  • Case t=truet=true, falseCount(t)=00falseCount(t) = 0 \geq 0
  • Case t=falset=false, falseCount(t)=10falseCount(t) = 1 \geq 0
  • Case t=if t1 then t2 else t3t=\texttt{if t1 then t2 else t3}
    • By IH, falseCount(t1)0falseCount(t1) \geq 0,falseCount(t2)0falseCount(t2) \geq 0, falseCount(t3)0falseCount(t3) \geq 0
    • By def of falseCountfalseCount, falseCount(t)0falseCount(t) \geq 0

Now that we have the proof, we name it lemma FCStepFC_{Step}. Next, we derive the theorem for multi-step.

Theorem: if ttt \rightarrow^* t', then falseCount(t)falseCount(t)falseCount(t') \leq falseCount(t).

Proof: By structural induction on tt,

  • IH: If t0t0t0 \rightarrow^* t0', where t0t0 is a subterm of tt, then falseCount(t0)falseCount(t0)falseCount(t0') \leq falseCount(t0).
  • Case analysis on the root rule in the derivation of ttt \rightarrow^* t'
    • Case MReflM_{Refl}: Then t=tt=t', then falseCount(t)=falseCount(t)falseCount(t)falseCount(t') = falseCount(t) \leq falseCount(t).
    • Case MStepM_{Step}, Then ttt\rightarrow t', By FCStepFC_{Step}, falseCount(t)falseCount(t)falseCount(t') \leq falseCount(t).
    • Case MtransitiveM_{transitive}, then ttt\rightarrow^* t'' and ttt'' \rightarrow^* t'.
      • WE ARE STUCK\textbf{WE ARE STUCK}

We cannot use our induction hypothesis here. There is no valid subterm relationship. There is no subterm we can grab for the transitive relationship.

Solution: we don’t do structural induction on tt. Instead we do a induction on the derivation tree.

We restart our proof. First, rewrite the theorem such that our derivation is given a name.

Theorem: If d:ttd: t\rightarrow^* t', then falseCount(t)falseCount(t)falseCount(t') \leq falseCount(t).

Proof: By structural induction on dd, which is the derivation of ttt \rightarrow^* t'

  • IH: If d0:t0t0d_0: t_0 \rightarrow t_0', where d0d_0 is a subderivation of dd,

then falseCount(t0)falseCount(t0)falseCount(t_0')\leq falseCount(t_0).

  • Case analysis on the root rule in the derivation of ttt \rightarrow^* t':
    • Case MReflM_{Refl}: Then t=tt=t', then falseCount(t)=falseCount(t)falseCount(t)falseCount(t') = falseCount(t) \leq falseCount(t).
    • Case MStepM_{Step}: Then ttt\rightarrow t', By FCStepFC_{Step}, falseCount(t)falseCount(t)falseCount(t') \leq falseCount(t).
    • Case MTransitiveM_{Transitive}: Then d1:ttd_1: t\rightarrow^* t'' and d2:ttd_2: t''\rightarrow^* t'.
      • By IH on d1d_1, falseCount(t)falseCount(t)falseCount(t'') \leq falseCount(t)
      • By IH on d2d_2, falseCount(t)falseCount(t)falseCount(t') \leq falseCount(t'')
      • Therefore, result follows by transitivity.

We can do this since derivation tree is a tree. It is recursively defined so we can perform structural induction as well.

Take home: we can apply this type of induction on the FCStepFC_{Step}

Theorem: If d:ttd: t \rightarrow^* t' and ttt \neq t', then there exists a tt'' such that ttt\rightarrow t'' and ttt'' \rightarrow^* t'.

Proof: by structural induction on dd.

  • IH: If d0:t0t0d_0: t_0 \rightarrow^* t_0' and t0t0t_0 \neq t_0' and d0d_0 is a subderivation of dd, then there exists a t0t_0''

such that t0t0t_0\rightarrow t_0'' and t0t0t_0'' \rightarrow^* t_0'.

  • Case analysis on the root rule in the derivation of ttt \rightarrow^* t':
    • Case MReflM_{Refl}: Then t=tt = t'. This is a contradiction. This case is not part of the theorem.
    • Case MStepM_{Step}: Then ttt \rightarrow t'
      • by MReflM_{Refl}, ttt' \rightarrow^* t'
      • Therefore, ttt\rightarrow t' and ttt' \rightarrow^* t'
    • Case MTransitiveM_{Transitive}: Then d1:tt0d_1: t\rightarrow^* t_0 and d2:t0td_2: t_0\rightarrow^* t'.
      • Case analysis on the relationship between tt and t0t_0:
      • This case analysis is to satisfy IH condition ttt \neq t'. So that we mathematically satisfies the IH.

        • Case tt0t \neq t_0:
          • By IH on d1d_1, ttnt\rightarrow t_n and tnt0t_n \rightarrow^* t_0.
          • Therefore, ttnt\rightarrow t_n and tnt0t_n \rightarrow^* t_0 and t0tt_0 \rightarrow^* t'
          • By MTransitiveM_{Transitive}, ttnt\rightarrow t_n and tntt_n \rightarrow^* t'.
          • result follows t=t0t'' = t_0.
        • Case t=t0t = t_0:
          • Case analysis on the relationship between t0t_0 and tt':
            • Case t0tt_0 \neq t': By IH, there exists tnt_n such that t0tnt_0 \rightarrow t_n and tntt_n \rightarrow^* t'. Since t=t0t=t_0, the result follows with t=tnt'' = t_n.
            • Case t0=tt_0 = t'. Then t=tt = t'. Contradiction.

Wow, a deeply nested structural induction. We have 3 levels of case analysis nested inside each other.

Static Type System

textbook: Chapter 8

Goal: Get rid of type error at compile time.

Previously, we have big-step, small-step and multi-step semantics judgement form. We need a new judgement form.

To build a type system, we assign each expression a type.

T ::= Bool | Int

The new typing judgement of the form t:Tt: T, which means “term tt has type TT.

b:BoolTBool\cfrac{}{ b: Bool } \quad {T_{Bool}}
n:IntTInt\cfrac{}{ n: Int } \quad {T_{Int}}
t1:Intt2:Intt1+t2:IntTPlus\cfrac{ t1: Int \quad t2: Int }{ t1 + t2: Int } \quad {T_{Plus}}

Note that we don’t have value evaluation here. But we have type assignment.

We can build derivation tree from these rules again.

1:IntTInt2:IntTInt1+2:IntTPlus\cfrac{ \cfrac{}{1: Int} T_{Int}\quad \cfrac{}{2: Int}T_{Int} }{ 1 + 2: Int }\quad T_{Plus}

We can have derivation that doesn’t work.

1:IntTIntERR!false:IntTInt1+false:IntTPlus\cfrac{ \cfrac{}{1: Int} T_{Int}\quad \cfrac{\bf ERR!}{false: Int}T_{Int} }{ 1 + false: Int }\quad T_{Plus}

Rule for Greater

t1:Intt2:Intt1>t2:BoolTGreater\cfrac{ t1: Int \quad t2: Int }{ t1 > t2: Bool } \quad {T_{Greater}}

Rule for If

t1:Boolt2:Tt3:Tif t1 then t2 else t3:TTIf\cfrac{ t1: Bool\quad t2: T\quad t3: T }{ \texttt{if t1 then t2 else t3}: T } \quad {T_{If}}
  • we are forcing the true branch and false branch to have the same type
  • figuring out eventually stuck expression is undecidable. A sound type system always be conservative and rule out program that technically run fine.
    if true then 0 else false
    This will evaluate to 0. But OCaml will prevent it from even running.

What if we want if true then 0 else false to pass the type check? We can add If-True rule.

t2:Tif true then t2 else t3:TTIfTrue\cfrac{ t2: T }{ \texttt{if true then t2 else t3}: T }\quad {T_{IfTrue}}

However, this rule is very limited since it requires the if to have syntactically have true as condition. Oh Bad.

We also cannot statically know what the condition will be, even we introduce a true or false type. So we like forcing if statement to have the same type.

Example: Build a type derivation tree

for if 3 > 2 then 0 else 1+2 build a derivation tree.

3:IntTInt2:IntTInt3>2:BoolTGreater0:IntTInt1:IntTInt2:IntTInt1+2:IntTPlusif 3 > 2 then 0 else 1+2:IntTIf\cfrac{ \cfrac{ \cfrac{}{3: Int}T_{Int} \quad \cfrac{}{2: Int}T_{Int} }{3 > 2: Bool} T_{Greater} \quad \cfrac{}{0: Int} T_{Int} \quad \cfrac{ \cfrac{}{1: Int} T_{Int} \cfrac{}{2: Int} T_{Int} }{1+2: Int}T_{Plus} }{ \texttt{if 3 > 2 then 0 else 1+2}: Int }\quad T_{If}

Type Soundness

How to we prove that our type system has eliminated type error before runtime?

“Well-typed programs don’t go wrong” (Milner)

Our goal: Well-typed programs are not eventually stuck.

Theorem (Type Soundness)

  • attempt 1: If t:Tt : T, then either tt is a value or exists tt' s.t. ttt \rightarrow^* t'.
    • this is not what we want. According to multi-step semantics. Any stuck expression multi-step to itself.
  • attempt 2: If t:Tt : T, then either tt is a value or exists tt' s.t. ttt \rightarrow t'.
    • Not good enough. We can take a step but it doesn’t mean we are not eventually stuck.
  • attempt 3: If t:Tt : T, then there exists a value vv such that tvt\rightarrow^* v.
    • what if program contains infinite loop. That means that we need to solve the halting problem in runtime.
  • attempt 4
    • tt is eventually stuck if ttt \rightarrow^* t' and tt' is stuck.
    • tt is stuck means there does not exist tt' s.t. ttt \rightarrow t'.
    • tt is not eventually stuck if for all tt' s.t. ttt \rightarrow^* t', tt' is not stuck:
      • Either tt' is a value or exists tt'' that ttt'\rightarrow t''.

Theorem (type soundness): if t:Tt: T and ttt \rightarrow^* t', either tt' is a value or there exists tt'' s.t. ttt' \rightarrow t''.

  • we allow infinite execution in this form.
  • big-step semantics does not allow us infinite execution.

How do we prove this?

Progress + Preservation

Progress and Preservation implies Type Soundness.

Lemma (Progress): “well typed terms are not stuck”.

  • If t:Tt : T, then either tt is a value or exists tt' s.t. ttt \rightarrow t'.

Lemma (Type Preservation): “stepping preserves well-typedness”

  • If t:Tt: T and ttt \rightarrow t', then t:Tt' : T.

This is actually stronger than the original version. It implies that

  • the expression that we step to is well-typed
  • the expression that we step to has the same type as before

while the original does not. Let’s prove them!

Lemma (Progress): If d:(t:T)d: (t : T), then either tt is a value or exists tt' s.t. ttt \rightarrow t'.

Proof: by structural induction on t.

  • IH: If t0:T0t_0 : T_0 and t0t_0 is a subterm of tt, then either t0t_0 is a value or exists t0t_0' s.t. t0t0t_0 \rightarrow t_0'.
  • Case analysis on the root rule in the derivation dd:
    • Case TIntT_{Int}: Then t=nt=n and T=IntT=Int. So tt is a value.
    • Case TPlusT_{Plus}: Then t=t1+t2t=t1+t2, t1:Intt1: Int and t2:Intt2: Int.
      • By IH, either t1t1 is a value or exists t1t1' s.t. t1t1t1\rightarrow t1'.
      • By IH, either t2t2 is a value or exists t2t2' s.t. t2t2t2\rightarrow t2'.
      • Case analysis on the possibilities of t1t1 and t2t2
        • Case t1t1 is a value and t2t2 is a value:
        • we need a lemma here that says a value of type Int is a number.

          • Since t1:Intt1: Int and t2:Intt2: Int, by Canonical Forms for Int, t1=n1t1 = n1 and t2=n2t2 = n2. Therefore by EPlusRedE_{PlusRed}, t1+t2nt1 + t2 \rightarrow n, where n=n1[[+]]n2n = n1 [[+]] n2.
        • Case t1t1t1 \rightarrow t1' and t2t2t2 \rightarrow t2':
          • Then t1+t2t1+t2t1 + t2 \rightarrow t1' + t2 by EPlus1E_{Plus1}
        • Case t1t1t1 \rightarrow t1' and t2t2 is a value:
          • Then t1+t2t1+t2t1 + t2 \rightarrow t1' + t2 by EPlus1E_{Plus1}
        • Case t1t1 is a value and t2t2t2 \rightarrow t2':
          • Then t1+t2t1+t2t1 + t2 \rightarrow t1 + t2' by EPlus2E_{Plus2}

Lemma (Canonical Forms for Int): If v:Intv: Int, then vv is a number nn.

  • we can prove this by case analysis on the rules without induction.

Alternatively, we can also do structural induction on dd.

  • IH: If d0:(t0:T0)d_0: (t_0 : T_0) and d0d_0 is a subderivation of dd, then either t0t_0 is a value or exists t0t_0' s.t. t0t0t_0 \rightarrow t_0'.

In turing complete language, we cannot use structural induction on subterms. We want to focus on structural induction on derivation.

We are going to redo our Progress proof from last time.

Lemma (Progress): If d:(t:T)d: (t : T), then either tt is a value or exists tt' s.t. ttt \rightarrow t'.

We have 2 canonical forms that we can use:

  • Lemma (Canonical Forms, Bool): If v:Boolv:Bool, then vv is a boolean value bb.
  • Lemma (Canonical Forms, Int): If v:Intv:Int, then vv is a integer value nn.

Proof: By induction on derivation dd.

  • IH: If d0:(t0:T0)d_0:(t_0:T_0), where d0d_0 is a subderivation of dd, then either t0t_0 is a value or exists t0t_0' such that t0t0t_0\rightarrow t_0'.
  • Case analysis on the root rule of dd:
    • Case TBoolT_{Bool}: Then t=bt=b and T=BoolT=Bool. Then tt is value.
    • Case TIfT_{If}: Then t=if t1 then t2 else t3t=\texttt{if t1 then t2 else t3} and t1:Boolt_1:Bool and
    t2:Tt_2:T and t3:Tt_3:T.
    • (i propose to use case analysis on t1t_1 but it goes into infinite regress)
    • By IH, t1t_1 is a value or exists t1t_1' such that t1t1t_1\rightarrow t_1'.
    • Case analysis on these cases.
      • Case t1t_1 is a value:
        • By Canonical Forms, Bool, either t1=truet_1=true or t1=falset_1=false
        • If former, tt steps by EIfTrueE_{IfTrue} otherwise by EIfFalseE_{IfFalse}
      • Case t1t1t_1\rightarrow t_1':
        • By EIfE_{If}, tt steps to if t1’ then t2 else t3\texttt{if t1' then t2 else t3}
    • Case TIntT_{Int}: Then t=nt=n and T=IntT=Int. Then tt is a value.
    • Case TPlusT_{Plus}: Then t1:Intt_1:Int and t2:Intt_2:Int.
      • By IH, t1t_1 is either a value or t1t1t_1\rightarrow t_1' (we can apply IH since t1:Boolt_1:Bool is a subderivation of t:Boolt:Bool).
      • By IH, t2t_2 is either a value or t2t2t_2\rightarrow t_2'.
      • Case analysis on these cases:
        • Case t1t_1 is a value and t2t_2 is a value.
          • By Canonical form, Int, t1t_1 is an integer value n1n_1 and t2t_2 is an integer value n2n_2. By EPlusRedE_{PlusRed}, tt steps.
        • Case t1t_1 is a value and t2t2t_2\rightarrow t_2'.
          • By EPlus2E_{Plus2}, tt steps.
        • Case t1t1t_1\rightarrow t_1' and t2t_2 is a value.
          • By EPlus1E_{Plus1}, tt steps.
        • Case t1t1t_1\rightarrow t_1' and t2t2t_2\rightarrow t_2'.
          • By EPlus1E_{Plus1}, tt steps.
    • (Other cases left as an exercise to the reader)

Note: a type system that rejects all program also satisfies type soundness, since t:Tt:T is never true, so the statement is true.

Now we focus on Preservation.

Lemma (Type Preservation): If d:(t:T)d: (t: T) and ttt \rightarrow t', then t:Tt' : T.

Proof: By induction on derivation dd.

  • IH: If d0:(t0:T0)d_0: (t_0: T_0), where d0d_0 is a subderivation of dd, and t0t0t_0 \rightarrow t_0', then t0:T0t_0' : T_0.
  • Case TBoolT_{Bool}: Then t=bt=b. Since tt is a value, it cannot step.
  • Case TIfT_{If}: Then t=if t1 then t2 else t3t=\texttt{if t1 then t2 else t3}, t1:Boolt_1:Bool, t2:Tt_2:T, t3:Tt_3:T.
    • case analysis on derivation ttt\rightarrow t':
      • Case EIfE_{If}: tif t1’ then t2 else t3t \rightarrow \texttt{if t1' then t2 else t3}.
        • By IH, t1:Boolt1': Bool
        • By TIfT_{If}, t:Tt':T
      • Case ETrueE_{True}: tt2t\rightarrow t_2: t2:Tt_2:T
      • Case EFalseE_{False}: tt3t\rightarrow t_3: t3:Tt_3:T

Lambda Calculus

An informal semantics for a function call:

  • (function x -> x + x) 4 \rightarrow 4+4 \rightarrow 8
  • (function x -> (function y -> x + y)) 4 \rightarrow (function y -> 4 + y)
  • ((function x -> (function y -> x + y)) 4) 3 \rightarrow
    (function y -> 4 + y) 3 \rightarrow 4 + 3 \rightarrow 7

untyped lambda calculus (chapter 5)

t ::= x | (function x -> t) | t t
x ::= variable names
  • t t is a function call.
  • this small language is turing complete.
  • λx.t\lambda x.t is the original syntax in lambda calculus for (function x -> t).

Some languages:

  • t is the body of (function x -> t)
  • x is bound within the body of (function x -> t)
  • a variable that occurs in t but is not bound in t is called a free variable

small-step operational semantics for the lambda calculus

v ::= (function x -> t)

Computation rule:

(function x -> t) t’t[xt]EAppBeta\cfrac{}{\texttt{(function x -> t) t'}\rightarrow t[x\mapsto t']} \quad E_{AppBeta}
  • \mapsto” is the substitution operation that sub xx with tt'.
  • EAppBetaE_{AppBeta} is also referred to the beta reduction
  • t[xt]t[x\mapsto t'] substitutes tt' for all free occurrences of xx in tt
    • e.g. (function x -> (function x -> x)) 5 \rightarrow (function x -> x)
    • aka. variable scoping

This language so powerful that it can define infinite loop.

Omega term

(function x -> x x) (function x -> x x)

Notice that this expression steps to itself.

Congruence rule

Where do we look for the next piece of work to do?

  • Describe the order of evaluation.
  • we go with call by value since that’s the most common
    • call-by-value semantics: call the leftmost function whose argument is a value

let id be shorthand for the identity function: (function x -> x).

((id (id id)) (id id))
  • which evaluation is called first? (the first (id id) by call-by-value)

Call-by-value

Call the leftmost function whose argument is a value.

t1t1t1 t2t1 t2EApp1\cfrac{ t_1 \rightarrow t_1' }{t_1\ t_2 \rightarrow t_1'\ t_2} \quad E_{App1}
t2t2v1 t2v1 t2EApp2\cfrac{ t_2 \rightarrow t_2' }{v_1\ t_2 \rightarrow v_1\ t_2'} \quad E_{App2}

According to these rules, we derive the tree for the example:

(id id)idEAppBeta(id (id id))(id id)EApp2((id (id id)) (id id))((id id) (id id))EApp1\cfrac{ \cfrac{ \cfrac{}{ (id\ id) \rightarrow id } E_{AppBeta} }{ (id\ (id\ id)) \rightarrow (id\ id) } \quad E_{App2} }{ ((id\ (id\ id))\ (id\ id)) \rightarrow ((id\ id)\ (id\ id)) } \quad E_{App1}
((id (id id)) (id id))((id id) (id id))(id (id id))(id id)id ((id\ (id\ id))\ (id\ id)) \rightarrow ((id\ id)\ (id\ id)) \rightarrow (id\ (id\ id)) \rightarrow (id\ id) \rightarrow id

Now we have fully defined the language. It is turing complete!

Note: some evaluation strategy will terminate while some other don’t. Let’s consider this program:

(function x -> id) omega

where omega is the infinite loop expression we defined above.

  • In a call-by-value semantics, we are required to evaluate omega first into a value, meaning it will run forever.
  • In lazy evaluation, we don’t evaluate argument until you need them. So, this program evaluate to just id

Note: for any 2 strategies that terminates, both will end up with the same value.

Examples of stuff that we can do with lambda calculus. Church came out we all of these. So we call them church boolean, church integers, etc. We try doing that in ocaml.

Church Booleans

But first, we take a detour to datatype

(* detour: booleans as a datatype *)
type mybool = True | False

let myand b1 b2 = 
  match b1 with 
    True -> b2 
  | False -> False

How do we define boolean in lambda calculus?

let ctrue = function t -> function f -> t
let cfalse = function t -> function f -> f

let cand b1 b2 = b1 b2 cfalse;;

Yes, that’s it! That’s how cand runs. If b1 is true, calling on b1 will return b2, whatever value it is. If b1 is false, calling on b1 will return cfalse, regardless of b2! Genius!

Church numerals

Note that nat stands for natural numbers. Yes, no negative here. Again, first we start with data type.

type mynat =  
  Zero 
| Succ of mynat

let rec mynat n1 n2 = 
  match n1 with 
    Zero -> n2
  | Succ n1' -> Succ (myplus n1' n2)

We defined a recursive plus, not efficient but it works.

let czero = function z -> function s -> z
let cOne = function z -> function s -> (s z)
let cTwo = function z -> function s -> s (s z)
  • z is the zero, and s is successor. Now we see how it knows what number it is.
let cplus n1 n2 = 
  (function z -> function s -> n1 (n2 z s) s)

How do we think of this? Well, we can treat the first argument z in any number as the number to start counting from, and s is how much we count up in each step. Calling cplus means create a new number that starts counting from n2, but we use the same count up of s. To make it truly correct n1 + n2, we have to force n2 to start from zero therefore we pass it the z and the fixed step up s.

Another way to think about it is from a type perspective. Assume we have some magic type Z. (not valid ocaml btw)

let czero = function z:Z -> function s:Z->Z -> z:Z

Therefore, we have to call n1 with a type Z and Z->Z

Simply-Typed Lambda Calculus

What is the error we are trying to catch? What are the stuck expressions?

  • We get stuck when trying to evaluate a variable that was never defined

(i.e., a free variable).

  • x is stuck
  • (x x) is stuck
  • (function x -> x) x is stuck (the rightmost x is free)

Again we start with the grammar, this time with a type.

t ::= x | (function x -> t) | t t
v ::= (function x -> t)

T ::= T -> T | Unit
(* an input type and an output type. *)
(* Unit is a base type, otherwise it is infinite recursion *)

Typecheck variable

x:??TVar\cfrac{ }{ x:?? } \quad T_{Var}

How do we typecheck a variable? What do we look for stuff? In a real programming languages, the types of arguments or variables are declared by the user.

We define a type environment:

a type environment is a finite mapping from variable names in scope to their types
We use the symbol Γ\Gamma.

Type judgment now has the form Γt:T\Gamma \vdash t: T.

  • “term tt has type TT in the context type environment Γ\Gamma

To type check a variable, we do a look up in the type env.

G(x)=TΓx:TTVar\cfrac{ G(x) = T }{ \Gamma\vdash x: T }\quad T_{Var}

Typecheck function

Γ,x:T1t:T2Γfunction xt:T1T2TFun\cfrac{ \Gamma,x:T_1 \vdash t: T_2 }{ \Gamma\vdash function\ x \rightarrow t: T_1 \rightarrow T_2 }\quad T_{Fun}
  • Γ,x:T1\Gamma,x:T_1 means add/overwrite the type of xx in Γ\Gamma with T1T_1.

This means if we can guess the type of x to be T1T_1. If it type checks it is fine. In reality, we have to try all possible types to find one that works. However, in real programming languages, we make the user declare the type of x.

To make our life easier, we add type declaration to our grammar.

t ::= x | (function x:T -> t) | t t
v ::= (function x:T -> t)

T ::= T -> T | Unit

The rule becomes

Γ,x:T1t:T2Γfunction x:T1t:T1T2TFun\cfrac{ \Gamma,x:T_1 \vdash t: T_2 }{ \Gamma\vdash function\ x:T_1 \rightarrow t: T_1 \rightarrow T_2 }\quad T_{Fun}

Typecheck function call

Γt1:T2TΓt2:T2Γt1 t2:TTApp\cfrac{ \Gamma\vdash t_1:T_2 \rightarrow T \quad \Gamma\vdash t_2:T_2 }{ \Gamma\vdash t_1\ t_2: T }\quad T_{App}

Let’s type check czero.

Γs:UnitUnitTVarΓz:UnitTVarΓ(s z):UnitTAppz:Unitfunction (s:Unit -> Unit) -> (s z):(UnitUnit)UnitTFun{}function z:Unit -> function (s:Unit -> Unit) -> (s z):Unit(UnitUnit)UnitTFun\cfrac{ \cfrac{ \cfrac{ \cfrac{}{\Gamma\vdash s:Unit\rightarrow Unit} \quad T_{Var} \quad \cfrac{}{\Gamma\vdash z:Unit}\quad T_{Var} }{ \Gamma \vdash \texttt{(s z)}: Unit } \quad T_{App} }{ z:Unit\vdash \texttt{function (s:Unit -> Unit) -> (s z)} : (Unit \rightarrow Unit) \rightarrow Unit } \quad T_{Fun} }{ \{\} \vdash \texttt{function z:Unit -> function (s:Unit -> Unit) -> (s z)}: Unit\rightarrow (Unit \rightarrow Unit) \rightarrow Unit }\quad T_{Fun}
  • abbreviate z:Unit,s:UnitUnitz:Unit, s:Unit\rightarrow Unit as Γ\Gamma

Modular typechecking: typecheck a function once and safely call it multiple times.

  • formalize this with a type substitution lemma: typing is preserved by substitution.

Theorem (Type Substitution): If Γ,x:T1t:T2\Gamma,x:T_1\vdash t:T_2 and Γv:T1\Gamma \vdash v:T_1, then Gt[xv]:T2G\vdash t[x \mapsto v]:T_2

We forgot to define substitution(\mapsto). Here we define substitution t[xv]t[x\mapsto v] formally. We assume that vv has no free variables.

  • x[xv]=vx[x\mapsto v] = v.
  • x[xv]=xx'[x\mapsto v] = x', where xxx'\neq x.
  • (function x -> t)[xv]=[x\mapsto v]= function(x -> t) (x is bound).
  • (function x' -> t)[xv]=[x\mapsto v]= function(x' -> t[xv][x\mapsto v]) where xxx'\neq x.
  • (t1 t2)[xv]=[x\mapsto v]= (t1[xv][x\mapsto v] t2[xv][x\mapsto v]).

Note: if we slap the type system into the language. Our language is no longer turing complete. It is terminates. (since type is finite) We need to introduce primitive of recursion.

Logic and typechecking

Notice TAppT_{App} is a simple logic that is known for a long time since Aristotle.

Γt1:T2TΓt2:T2Γt1 t2:TTApp\cfrac{ \Gamma\vdash t_1:T_2 \rightarrow T \quad \Gamma\vdash t_2:T_2 }{ \Gamma\vdash t_1\ t_2: T }\quad T_{App}

If we treat our function type as an implication, i.e. T2TT_2\rightarrow T, this is means that

PQPQ\cfrac{P\Rightarrow Q \quad P}{Q}

If we know PP implies QQ and we know PP, we know QQ.

Curry-Howard Isomorphism

Types are also theorems. Programs are also proofs of theorem (proof objects or proof witnesses).

“natural deduction”

For each logical connective, you have an “introduction rule”, which says how to prove facts of that form, and an “elimination rule”, which says how to use facts of that form.

  • Introduction rule
G,PQGP    QImplicationIntro\cfrac{ G, P \vdash Q }{ G \vdash P \implies Q } \quad Implication_{Intro}
  • elimination rule
GP    QGPGQImplicationElim\cfrac{ G \vdash P \implies Q\quad G \vdash P }{ G \vdash Q } \quad Implication_{Elim}
  • assumption
P in QGPAssumption\cfrac{P \text{ in } Q}{G\vdash P} \quad Assumption

The simply-typed calculus is a constructive propositional logic.

  • every proof comes with an explicit proof object that witnesses the proof

A proposition PP is valid if and only if there exists a term tt such that {}t:P\{\}\vdash t:P.

Let’s suppose we have an infinite number of uninterpreted type constants A,B,C,...A,B,C,...

Example:

{}(function x:Px):PP\{\} \vdash {\tt (function\ x:P \rightarrow x)} : P \rightarrow P

Note if we introduce base types like booleans or integers into the lambda calculus, the proof system breaks down and does not follow strict logic.

  • Can you write a program in simply-typed lambda calculus that has type PPP\rightarrow P?
    • yes: (function x:P -> x)
  • What about type PQP\rightarrow Q?
    • no
  • What about type P(QP)P\rightarrow(Q\rightarrow P)? eqiv. to PQPP \rightarrow Q \rightarrow P
    • Yes: (function x:P -> function y:Q -> x)
  • What about type (PQR)(QPR)(P \rightarrow Q \rightarrow R) \rightarrow (Q \rightarrow P \rightarrow R)?
    • Yes:
    function f:(P->Q->R) -> function q:Q -> function p:P -> f p q

Logical Conjunction in type system

We add an “and” into our type system. This is really just adding a tuple to our system.

T ::= ... | T ∧ T
t ::= ... | (t, t) | fst t | snd t
Γt1:T1Γt2:T2Γ(t1,t2):T1T2AndIntro\cfrac{ \Gamma\vdash t_1: T_1 \quad \Gamma\vdash t_2: T_2 }{ \Gamma\vdash (t_1, t_2): T_1 \land T_2 } \quad And_{Intro}
Γt:T1T2Γfst t:T1AndElim1\cfrac{ \Gamma\vdash t: T_1 \land T_2 }{ \Gamma\vdash \texttt{fst t}: T_1 } \quad And_{Elim1}
Γt:T1T2Γsnd t:T2AndElim2\cfrac{ \Gamma\vdash t: T_1 \land T_2 }{ \Gamma\vdash \texttt{snd t}: T_2 } \quad And_{Elim2}

That means we can derive data structure from logic.

Example: commutativity of logical conjunction

(PQ)(QP)(P\land Q) \rightarrow (Q\land P)

Find a program that has that type:

  • ans: (function x: (P ∧ Q) -> (snd x, fst x))
  • in ocaml with help of type inference: function x -> (snd x, fst x)

Now we define the operational semantics. First, we have to notice new values. Now we can have tuples as value as well.

v ::= (function x:T -> t) | (v, v)

Exercise: write a small-step operational semantics for tuple.

t1t1(t1,t2)(t1,t2)EPair1\cfrac{ t_1 \rightarrow t_1' }{ (t_1, t_2) \rightarrow (t_1', t_2) } \quad E_{Pair1}
t2t2(v1,t2)(v1,t2)EPair2\cfrac{ t_2 \rightarrow t_2' }{ (v_1, t_2) \rightarrow (v_1, t_2') } \quad E_{Pair2}

Notice that we do not have computational rule for tuple to construct a tuple (unlike function or whatever). This is because tuple is already a value.

ttfst tfst tEFst\cfrac{ t \rightarrow t' }{ \texttt{fst }t \rightarrow \texttt{fst }t' } \quad E_{Fst}
fst (v1,v2)v1EFstRed\cfrac{}{ \texttt{fst }(v_1, v_2) \rightarrow v_1 } \quad E_{FstRed}
  • we need both elements to be value because if second element has side effect we want to carry it out, so we evaluate second element until it is a value.
ttsnd tsnd tESnd\cfrac{ t\rightarrow t' }{ \texttt{snd }t \rightarrow \texttt{snd }t' } \quad E_{Snd}
snd (v1,v2)v2ESndRed\cfrac{}{ \texttt{snd }(v_1, v_2) \rightarrow v_2 } \quad E_{SndRed}

Logical Disjunction

In ocaml, we know that type unions are disjunction.

type intorbool = I of int | B of bool;;

We need the tag I and B are for a type safe way to use unions.

T ::= ... | T ∨ T

The logical rules are

Γt1:T1Γleft t1:T1T2OrIntro1\cfrac{ \Gamma\vdash t_1:T_1 }{\Gamma \vdash {\tt left}\ t_1: T_1 \lor T_2} \quad Or_{Intro1}
  • notice that T2T_2 is unknown
  • the rule is not algorithmic, we have to guess what T2T_2 is
  • in real programming languages, we know ahead what type left is
  • like in ocaml, we have to declare it upfront
    type LeftOrRight = Left of a | Right of b;;
Γt2:T2Γright t2:T1T2OrIntro2\cfrac{ \Gamma\vdash t_2: T_2 }{\Gamma\vdash{\tt right}\ t_2: T_1 \lor T_2} \quad Or_{Intro2}
Γt:T1T2Γ,x:T1t1:TΓ,y:T2t2:TΓ(match t with left x -> t1 | right y -> t2):TOrElim\cfrac{ \Gamma \vdash t: T_1 \lor T_2\quad \Gamma, x: T_1 \vdash t1: T\quad \Gamma, y: T_2 \vdash t2: T }{ \Gamma \vdash \texttt{(match t with left x -> t1 | right y -> t2)} : T } \quad Or_{Elim}

This is saying that if we can do something (getting TT) assuming T1T_1 and if we do something (getting TT) assuming T2T_2. We can conclude that we can for sure get something of type TT back.

To simplify this, we limit union size to only 2. We only use left and right as our constructor names.

t ::= ... | left t | right t 
    | (match t with left x -> t '|' right x -> t)

Example: commutativity of disjunction

Write a program whose type is (PQ)(QP)(P \lor Q) \rightarrow (Q\lor P)

function u: (P ∨ Q) -> 
  match u with 
    left x -> right x
  | right y -> left y

The set of rules defines a relation and it can be many-to-many. We are allowed to have

0:Bool\cfrac{}{0:Bool}
0:Int\cfrac{}{0:Int}

So 0 can have both types. In preservation theorem, we only need to prove it preserves type as long as one of the possible type is the matching one.

In semantics, what if we have two paths to take? Then our language is non-deterministic, which is completely okay! Since multi-threaded languages are non-deterministic so to model that we would need many-to-many relation even in semantics.

Back to pairs:

With pairs as primitive, we get multiple arguments and multiple returns for free.

function x:(Int, Bool) -> x
(* returns a (Int, Bool) *)

Back to disjunction

Derivation for flipping disjunction from last time:

function u: (P ∨ Q) -> 
  match u with 
    left x -> right x
  | right y -> left y
u:(PQ)u:(PQ)TVaru:(PQ),x:Px:PTVaru:(PQ),x:Pright x:QPTOrIntro2u:(PQ)match u with ...:QPOrElim{}PTFun\cfrac{ \cfrac{ \cfrac{}{u:(P ∨ Q) \vdash u:(P ∨ Q)} T_{Var} \quad \cfrac{ \cfrac{}{u:(P ∨ Q),x:P\vdash x:P} T_{Var} }{u:(P ∨ Q),x:P\vdash right\ x: Q ∨ P} T_{Or-Intro2} }{ u: (P ∨ Q) \vdash \texttt{match u with ...}: Q ∨ P } Or_{Elim} }{ \{\} \vdash \texttt{P} } T_{Fun}

The tree is not complete since we are missing subtree for right y but do it yourself.

Semantics for union type:

ttleft tleft tELeft\cfrac{ t \rightarrow t' }{\tt left\ t \rightarrow left\ t'} E_{Left}
ttright tright tERight\cfrac{ t \rightarrow t' }{\tt right\ t \rightarrow right\ t'} E_{Right}
ttmatch t with left x -> t1 ‘|’ right x -> t2match t’ with left x -> t1 ‘|’ right x -> t2EMatch\cfrac{ t \rightarrow t' }{ \begin{array}{c} \texttt{match t with left x -> t1 `|' right x -> t2}\\ \rightarrow\\ \texttt{match t' with left x -> t1 `|' right x -> t2}\\ \end{array} } E_{Match}
match left v with left x -> t1 ‘|’ right x -> t2t1[xv]EMatchLeft\cfrac{}{ \begin{array}{c} \texttt{match left v with left x -> t1 `|' right x -> t2}\\ \rightarrow\\ \tt t1[x\mapsto v] \end{array} } E_{MatchLeft}
match right v with left x -> t1 ‘|’ right x -> t2t2[xv]EMatchRight\cfrac{}{ \begin{array}{c} \texttt{match right v with left x -> t1 `|' right x -> t2}\\ \rightarrow\\ \tt t2[x\mapsto v] \end{array} } E_{MatchRight}

Type True

What if we introduce type True?

T ::= ... | True

What’s the equivalent in real programming languages? In OCaml, it is Unit. Why is it useful? We want function that takes in no argument or without no return value. For uniformity, we use Unit.

Γ():TrueTrueIntro\cfrac{}{ \Gamma \vdash (): True } \quad True_{Intro}
  • note that () is the syntactic value of unit type
  • write an example of function with type (P->True)
    (function x:P -> ()) : P -> True

What about False?

Γt:FalseΓt:TFalseElim\cfrac{ \Gamma\vdash t: False }{ \Gamma\vdash t: T }\quad False_{Elim}
  • from false, we get everything

~T as the shorthand for (T -> False)

Example: (~P ∧ (P ∨ Q)) -> Q

(function x: (P -> False)(P ∨ Q) -> 
  match snd x with
    left p -> (fst x) p
  | right q -> q
)

Since from (P -> False) we can derive everything, in left p case we extract ~P with fst then passing p to it to produce False.

In real programming language, a non-terminating program is typed False, since we can assign any return type to a non-terminating function.

# let rec infLoop() = infLoop();;
val infLoop : unit -> 'a = <fun>

OCaml, type the return as 'a meaning we can choose any type, it will pass type checking but will run forever.

Adding features

Local Variables

Currently, we have variables either through function or match case. In OCaml, we can use let

t ::= ... | let x=t in t

Local variables:

t1t1let x=t1 in t2let x=t1’ in t2ELet\cfrac{ t1 \rightarrow t1' }{ \texttt{let x=t1 in t2} \rightarrow \texttt{let x=t1' in t2} } \quad E_{Let}
let x=v1 in t2t2[xv1]ELetRed\cfrac{}{ \texttt{let x=v1 in t2} \rightarrow \texttt{t2[x$\mapsto$v1]} } \quad E_{LetRed}
Γt1:T1Γ,x:T1t2:TΓlet x=t1 in t2:T\cfrac{ \Gamma \vdash t1: T1 \quad \Gamma, x:T1 \vdash t2: T }{ \Gamma\vdash \texttt{let x=t1 in t2}: T }

Is this a syntactic sugar?

Is let x=t1 in t2 equivalent to (function x:T -> t2) t1?

  • not quite, since we do not know the type of x, until we have typechecked

Sequencing (side-effect)

t ::= ... | t;t

we don’t have side-effect yet but it would be useful later.

t1t1t1;t2t1;t2ESeq\cfrac{ t1 \rightarrow t1' }{ t1;t2 \rightarrow t1';t2 } \quad E_{Seq}
();t2t2ESeqRed\cfrac{}{ ();t2 \rightarrow t2 } \quad E_{SeqRed}
  • We force any statement to evaluate to the (), the True type (our representation of type Unit). So we indicate clearly we don’t care about the value.
Γt1:TrueΓt2:TΓt1;t2:TTSeq\cfrac{ \Gamma\vdash t1: True \quad \Gamma\vdash t2: T }{ \Gamma\vdash t1;t2:T } \quad T_{Seq}

t1;t2 is equivalent to (function x:True -> t2) t1. Therefore, this is a syntactic sugar.

In Ocaml, t1;t2 is syntactic sugar for let _ = t1 in t2.

Recursion

t ::= ... | let rec x=v in t
  • the difference between let x=t1 in t2 and let rec x=v in t2 is that x is not available in the scope of t1 in the former while x is in scope in v for rec.
  • by making the right hand side a value v, we make sure that it is a function instead of other terms so we don’t get something like
    let rec x = x + 1 in ...
  • but why v not only (function x:T -> t)? It is more general since we can define a pair of function, therefore it is mutually recursive.
let rec x=v1 in t2let x = v1[xlet rec x=v1 in x] in t2ELetRec\cfrac{}{ \texttt{let rec x=v1 in t2} \rightarrow \texttt{let x = v1[x$\mapsto$let rec x=v1 in x] in t2} } \quad E_{LetRec}

This rules says we simply redefine the instance of variable name in the body with an inline declaration of itself.

Example

let rec fact = 
  function n -> if n=0 then 1 else n * fact(n-1) in fact 5

(* ---> *)

let fact = 
  function n -> if n=0 then 1 else 
    n * (let rec fact = function n -> ... fact)(n-1) in fact 5

(* ---> *)
(* reducing the let in *)

(function n -> ...) 5

(* ---> *)

if 5=0 then 1 else 
  5 * (let rec fact = function n -> ... in fact)(5-1)

(* --->* *)

5 * (let rec fact = function n -> ... in fact)(5-1)

(* --->* *)

5 * (function n -> if n=0 then 1 else n * (let rec fact = function n -> ... in fact)) (3)

Notice at the end, on the right hand side, the expression is back to calling a fact function with a number. Hey, recursion!

Does this type check rule work?

Γv1:T1Γ,x:T1t2:TΓlet rec x=v1 in t2:T\cfrac{ \Gamma\vdash v1: T1 \quad \Gamma,x:T1\vdash t2: T }{ \Gamma\vdash \texttt{let rec x=v1 in t2}: T }

It does not! To type check v1, we need to add x into the scope of v1. Any instance of x in v1 will make v1 to do the typecheck,

Γ,x:T1v1:T1Γ,x:T1t2:TΓlet rec x=v1 in t2:T\cfrac{ \Gamma,x:T1\vdash v1: T1 \quad \Gamma,x:T1\vdash t2: T }{ \Gamma\vdash \texttt{let rec x=v1 in t2}: T }

Notice how this matches our comparison between let in and let rec, since x is also in the scope of v1.

We guess the type of x and hope it works. In real programming languages, we declare both the return and argument types:

int fact(int n) {
  return n == 0 ? 1 : fact(n-1) * n;
}

OCaml figure this out by using type inference.

The book does so by defining a fix point. Go read the book.

At this point, our language is turing complete

Example: infinite loop

let rec infiniteLoop = 
  (function x:True -> infiniteLoop x) in infiniteLoop ()

How do we type check this?

infiniteLoop:TrueBoolfunction x:True -> infiniteLoop x:TrueBool{} our infinite loop expression \cfrac{ \begin{array}{c} {\tt infiniteLoop}: True \rightarrow Bool \vdash \\ \texttt{function x:True -> infiniteLoop x}: True \rightarrow Bool \end{array} }{ \{\} \vdash \text{ our infinite loop expression } }

We guessed the typed to be TrueBoolTrue\rightarrow Bool. And the derivation tree is actually valid. You can derive the whole derivation tree. Again from before, we know that an infinite loop that does not terminate can return any type, and Bool is one of any type.

Therefore, we can derive infinite number of types for infiniteLoop.

However, the logic system becomes unsound as soon as we have infinite loop. Since infinite loop allows us to go from any thing to go to anything.

Type Inference

Consider the simply-typed lambda calculus plus integers:

t ::= n | t + t | x | function x:T -> t | t t
T::= Int | T -> T

Type checking judgement is a ternary relation between type env Γ\Gamma, term tt and type TT.

Typechecking problem: Given Γ\Gamma and tt, does there exist a type TT such that Γt:T\Gamma\vdash t:T?

It was easy to type check function since the input is annotated with a type T. However, in OCaml, we don’t need to declare type for function since the language infer this for us. How do we do typechecking in those cases?

We define the problem of type inference.

Define an “erased” version of our language:

s ::= n | s + s | x | function x -> s | s s

Define an “erase” function from t to s:

  • erase(n) = n
  • erase(t1 + t2) = erase(t1) + erase(t2)
  • erase(x) = x
  • erase(function x:T1 -> t) = function x -> erase(t)
  • erase(t1 t2) = erase(t1) erase(t2)

Type inference problem: Given Γ\Gamma and ss, does there exist a term tt and type TT such that Γt:T\Gamma\vdash t:T and erase(t) = s?

Example: Given Γ={}\Gamma=\{\} and s=s=function x -> x + 1, what is tt? What is TT?

  • t=t=function x:Int -> x + 1
  • T=IntIntT=Int\rightarrow Int

Type inference broken into 2 phases.

  1. constraints generation
  2. constraints solving

We introduce a new meta-variable for “types”.

S ::= Int | S -> S | X
X ::= type variable

Constraint Generation

How to turn typechecking rules into type inference rules?

  1. whenever a typechecking rules would have to “guess” a type, instead we create a fresh variable
  2. our type inference rules will never fail
    • 1 + true fails on type check
    • but there is a valid derivation in type inference
    • produce a set of constraints that must be satisfied
    • later, if the constraints cannot be solved, we reject

Type inference judgement: tinfer(Γ,s)=(S,C){\rm tinfer}(\Gamma, s)=(S, C)

  • in the book, Γs:SC\Gamma\vdash s: S \mid C
  • CC is a set of type equality constraints of the form S1=S2S_1=S_2.

Type inference for Int

freshXtinfer(Γ,n)=(X,{X=Int})INum\cfrac{ {\rm fresh} X }{ {\rm tinfer}(\Gamma, n) = (X, \{ X = Int\}) } \quad I_{Num}
  • freshX{\rm fresh}X grabs a free type variable. Then, we impose constraint X=IntX=Int.
tinfer(Γ,s1)=(S1,C1)tinfer(Γ,s2)=(S2,C2)freshXtinfer(Γ,s1+s2)=(X,{X=Int,S1=Int,S2=Int}C1C2)IPlus\cfrac{ {\rm tinfer}(\Gamma, s_1) = (S_1, C_1) \quad {\rm tinfer}(\Gamma, s_2) = (S_2, C_2) \quad {\rm fresh}X }{ {\rm tinfer}(\Gamma, s_1 + s_2) = (X, \{ X=Int, S_1=Int, S_2=Int\} \cup C_1 \cup C_2) } \quad I_{Plus}

Why does tinfer(Γ,s1){\rm tinfer}(\Gamma, s_1) returns a type variable S1S_1?

  • if we force it to have IntInt, then we don’t have the type inference
  • example: x+5. We cannot resolve x to concrete type yet!

Example of type infer derivation: 1+2

tinfer(1)=(X1,{X1=Int})INumtinfer(2)=(X2,{X2=Int})INumtinfer(1+2)=(X3,{X1=Int,X2=Int,X3=Int})IPlus\cfrac{ \cfrac{}{ {\rm tinfer}(1) = (X_1, \{X_1 = Int\}) } I_{Num} \quad \cfrac{}{ {\rm tinfer}(2) = (X_2, \{X_2 = Int\}) } I_{Num} }{ {\rm tinfer}(1+2) = (X_3, \{ X_1 = Int, X_2 = Int, X_3 = Int \}) } \quad I_{Plus}
  • assumption Γ=\Gamma=\empty implicitly

One that has no solution: 1 + true

tinfer(1)=(X1,{X1=Int})INumtinfer(true)=(X2,{X2=Bool})INumtinfer(1+true)=(X3,{X1=Int,X2=Int,X2=Bool,X3=Int})IPlus\cfrac{ \cfrac{}{ {\rm tinfer}(1) = (X_1, \{X_1 = Int\}) } I_{Num} \quad \cfrac{}{ {\rm tinfer}(true) = (X_2, \{X_2 = Bool\}) } I_{Num} }{ {\rm tinfer}(1+true) = (X_3, \{ X_1 = Int, X_2 = Int, X_2=Bool, X_3 = Int \}) } \quad I_{Plus}
  • X2=IntX_2=Int and X2=BoolX_2=Bool is unsolvable
Γ(x)=Stinfer(Γ,x)=(S,{})IVar\cfrac{ \Gamma(x) = S }{ {\rm tinfer}(\Gamma, x) = (S, \{\}) } \quad I_{Var}
freshXtinfer((Γ,x:X),s)=(S,C)freshYtinfer(Γ,function x -> s)=(Y,{Y=XS}C)IFunction\cfrac{ {\rm fresh}X \quad {\rm tinfer}((\Gamma, x:X), s) = (S, C) \quad {\rm fresh}Y }{ {\rm tinfer}(\Gamma, \texttt{function x -> s}) = (Y, \{ Y = X\rightarrow S \}\cup C) } \quad I_{Function}
  • we use rule 1, guess the type for x by assigning a type variable XX
  • then, we put that into the type env

Example for type infer on function

tinfer(x:X,x)=(X,{})IVartinfer(x:X,1)=(X1,{X1=Int})INumtinfer(x:X,x+1)=(X2,{X1=Int,X2=Int,X=Int})IPlustinfer({},function x -> x + 1)=(X3,{X3=XX2,X2=Int,X=Int,X1=Int})IFunction\cfrac{ \cfrac{ \cfrac{ }{ {\rm tinfer}(x:X, x) = (X, \{\}) } I_{Var} \cfrac{}{ {\rm tinfer}(x:X, 1) = (X_1, \{X_1 = Int\}) } I_{Num} }{ {\rm tinfer}(x:X, x+1) = (X_2, \{X_1 = Int, X_2=Int, X=Int\}) } I_{Plus} }{ \begin{array}{ll} & {\rm tinfer}(\{\}, \texttt{function x -> x + 1}) \\ =&(X_3, \{X_3=X\rightarrow X_2, X_2=Int, X=Int, X_1=Int\}) \end{array} } \quad I_{Function}
  • notice the benefit of not eagerly requiring x to have IntInt in the IPlusI_{Plus} check!
tinfer(Γ,s1)=(S1,C1)tinfer(Γ,s2)=(S2,C2)freshXtinfer(Γ,s1 s2)=(X,{S1=S2X}C1C2)IApp\cfrac{ {\rm tinfer}(\Gamma, s_1) = (S_1, C_1) \quad {\rm tinfer}(\Gamma, s_2) = (S_2, C_2) \quad {\rm fresh}X }{ {\rm tinfer}(\Gamma, s_1\ s_2) = (X, \{ S_1 = S_2 \rightarrow X \}\cup C_1 \cup C_2) } \quad I_{App}

Example: function f -> f 0

tinfer({f:F},f)=(F,{})IVartinfer({f:F},0)=(X,{X=Int})INumtinfer({f:F},f 0)=(Y,{F=XY,X=Int})IApptinfer({},function f -> f 0)=(Z,{Z=FY,F=XY,X=Int})IFunction\cfrac{ \cfrac{ \cfrac{}{ {\rm tinfer}(\{f:F\},f) = (F, \{\}) } I_{Var} \quad \cfrac{}{ {\rm tinfer}(\{f:F\}, 0) = (X, \{ X = Int\}) } I_{Num} }{ {\rm tinfer}(\{f: F\}, \texttt{f 0}) = (Y, \{ F = X \rightarrow Y, X = Int \}) } \quad I_{App} }{ {\rm tinfer}(\{\},\texttt{function f -> f 0}) = (Z, \{ Z = F \rightarrow Y, F = X \rightarrow Y, X = Int \}) } \quad I_{Function}

A substitution σ\sigma is a mapping from type variable to types.

  • A substitution σ\sigma satisfies C={S1=S1,...,Sn=Sn}C=\{S_1 = S_1',...,S_n=S_n'\} if σ(Si)\sigma(S_i) is syntactically equal to σ(Si)\sigma(S_i') for all ii.

What is the substitution solution to function f -> f 0?

{X:Int,Y:Bool,F:IntBool,Z:(IntBool)Bool}\{ X: Int, Y: Bool, F: Int \rightarrow Bool, Z: (Int \rightarrow Bool) \rightarrow Bool \}
  • notice that we can choose any type for YY. Here we just choose BoolBool

Correctness of Constraint Generation

Intuitively we want to prove that the constraints we generate capture only and all of the possible typings for a program.

  • Soundness of Constraint Generation

    • If tinfer({},s)=(S,C){\rm tinfer}(\{\}, s) = (S, C) and σ\sigma satisfies CC, then there exists term tt such that erase(t) = s and {}t:σ(S)\{\}\vdash t:\sigma(S)
  • Completeness of Constraint Generation

    • If {}t:T\{\}\vdash t:T and s=erase(t),
      then tinfer({},s)=(S,C){\rm tinfer}(\{\}, s) = (S, C) and there exists a σ\sigma such that
      σ\sigma satisfies CC and σ(S)=T\sigma(S) = T

We will not be proving them.

Constraints Solving

How do we solve constraints?

  • we can plug them into Prolog. Consider {Z=FY,F=XY,X=Int})\{ Z = F \rightarrow Y, F = X \rightarrow Y, X = Int \})

    Z=arrow(F, Y), F=arrow(X, Y), X=int.

    gives the solution

    Z=arrow(arrow(int, Y), Y)
    X=int
    F=arrow(int, Y)
  • OCaml type inference system does something similar

We will explore constraint solving next time.

Type inference broken into 2 phases.

  1. constraints generation
  2. constraints solving

We introduce a new meta-variable for “types”.

S ::= Int | S -> S | X
X ::= type variable

Polymorphic Lambda Calculus (System F, Second-Order Lambda Calculus)

The grammars

t ::= ... | function x:T -> t | function X -> t | t [T]
v ::= ... | function X -> t
T ::= Unit | Int | Bool | T -> T | X 
    | ...(all the tuples and stuff) 
    | forall X.T

New type forall X.T Example:

  • identity function should have the type forall X.X->X

We also have a function that takes a type! Identity function in polymorphic lambda calculus:

(function X -> function x:X -> x)

It has type forall X. X -> X. To call it

let id = (function X -> function x:X -> x) in 
    id [Bool] true

Example: swap

In OCaml, we can write

let swap = function p -> (snd p, fst p) in
    swap (34, true)

Write this program in polymorphic lambda.

let swap = function X -> 
           function Y -> 
           function p:(X ∧ Y) -> (snd p, fst p) in 
    swap [Int] [Bool] (34, true)

The type for swap is forall X. forall Y. (X ∧ Y) -> (Y ∧ X).

Now we go back to the bad example of function id -> (id 34, id true). Now we can make it valid.

let notSoBad = function id:forall X.X->X -> 
    (id [Int] 34, id [Bool] true)

This works because user don’t get to decide the concrete type. The implementation decides it!

Now we can typecheck self-application.

function x:forall X.X->X -> (x [forall X.X->X] x)

This function is now forall X.X->X -> forall X.X->X. Same reason why id [forall X. X->X] id type checks.

The type inference for this language is undecidable. That’s why OCaml only stick to let polymorphism to make type inference decidable.

Operational semantics for polymorphic lambda

ttt [T]t [T]ETApp\cfrac{ t\rightarrow t' }{ t\ [T] \rightarrow t'\ [T] } \quad E_{TApp}
(function X -> t) [T]t[XT]ETAppRed\cfrac{}{ \texttt{(function X -> t) [T]} \rightarrow t[X\mapsto T] } \quad E_{TAppRed}

Example: (function X -> function x:X -> x) [Int] 34

  • steps to (function x:Int -> x) 34
  • steps to 34

Typecheck for polymorphic lambda

Γ,Xt:TΓfunction X -> t:forall X.TTTFun\cfrac{ \Gamma,X \vdash t: T }{ \Gamma\vdash \texttt{function X -> t} : \text{forall } X.T } \quad T_{TFun}

We put XX into Γ\Gamma to keep track of the type variables which will use later.

Example: typechecking id

X,x:Xx:XTVarXfunction x:X -> x:XXTFun{}function X -> function x:X -> x:forall X.XXTTFun\cfrac{ \cfrac{ \cfrac{}{ X,x:X \vdash x:X } \quad T_{Var} }{ X\vdash \texttt{function x:X -> x}: X\rightarrow X } \quad T_{Fun} }{ \{\}\vdash \texttt{function X -> function x:X -> x}: \text{forall } X.X\rightarrow X } \quad T_{TFun}
Γt:forall X.TΓt [T0]:T[XT0]TTApp\cfrac{ \Gamma \vdash t: \text{forall } X.T }{ \Gamma \vdash t\ [T_0]: T[X\mapsto T_0] } \quad T_{TApp}

Example: function id:forall X.X->X -> id [Int] 34

Γid:forall X.XXTVarΓid [Int]:IntIntTTAppΓ34:IntTNumid:forall X.XXid [Int] 34:IntTApp{}function id:forall X.X->X -> id [Int] 34:(forall X.XX)IntTFun\cfrac{ \cfrac{ \cfrac{ \cfrac{}{ \Gamma \vdash id:\text{forall }X.X\rightarrow X } T_{Var} }{ \Gamma \vdash \texttt{id [Int]}: Int \rightarrow Int } T_{TApp} \quad \cfrac{}{ \Gamma \vdash 34:Int } T_{Num} }{ id:\text{forall }X.X\rightarrow X \vdash \texttt{id [Int] 34} : Int } T_{App} }{ \{\}\vdash \texttt{function id:forall X.X->X -> id [Int] 34}: (\text{forall }X.X\rightarrow X)\rightarrow Int } T_{Fun}

Why is this “second-order”?

  • First-order logic: forall x.P(x)
  • Second-order logic: forall X.P(X)

We skipped over first-order logic. Second order logic is more natural in programming languages.

Example: the identity function is a witness of a theorem of forall X. X-> X or XX,XX\rightarrow X, \forall X

Existential quantification

Existential typing is a core of information hiding.

Consider a counter class in Java:

class Counter {
  private int c = 0;
  public void inc() { c++; }
  public void reset() { c = 0; }
  public void get() { return c; }
}

We hide the information of implementation using private field.

We implement something like this in OCaml as a tuple of functions.

let counter = (
  (function () -> 0),
  (function x -> x + 1), (* takes a counter and increment it *)
  (function x -> 0), 
  (function x -> x)
)

These are typed as

(Unit -> int)
(int -> int)
('a -> int)
('b -> 'b)

We are not hiding the representation as int. To hide the representation, we give this tuple of functions the following types:

exists X. (Unit -> X, X -> X, X -> X, X -> Int)

Now the client does not know what X is.

Proof assistant

  • Proof assistant is a programming environment where you can also prove properties about your code.
  • functional programming language called Gallina
    • define functions and define datatypes
    • both powerful, much more expressive than what we can do in OCaml

Defining boolean

In OCaml,

type bool = True | False

In Gallina,

Inductive bool : Type :=
  true : bool
| false : bool.

Defining negate

In OCaml,

let negate b = match b with 
  true -> false
| false -> true

In Gallina,

Definition negate (b:bool) := 
  match b with
    true => false
  | false => true
  end.

Running computation:

Computer (negate (negate true)).

Writing a lemma

Lemma doubleNegation:
  forall b, negate(negate b) = b.

Proofing the lemma

Lemma doubleNegation:
  forall b, negate(negate b) = b.
Proof.
  intros. destruct b.
  - unfold negate. reflexivity.
  - unfold negate. reflexivity.
Qed.
  • intros is the assumptions that we make. We type of the variables will be inferred from the type system.
  • destruct is the tactic of proof, which is case analysis
  • unfold means we unfold the definition of negate in the proof
  • reflexivity: when we unfold negate, we get true equals true. We explicitly use the lemma of reflexivity such that true = true is a true theorem.
  • we have 2 sub case true and false so we repeat unfold twice. In Proof mode, we know what are the subcases. They should be in the order that they show up in the type def.

Defining list

In OCaml,

type boollist = nil | cons of bool * boollist

In Gallina,

Inductive boollist: Type :=
  nil : boollist
| cons : bool -> boolllist -> boollist.

We write the def of cons as a constructor function, we have 2 arguments: a boolean and a list of bools and the result.

Defining Recursion

Fixpoint append (l1 l2 : boollist) := 
  match l1 with
    nil => l2
  | cons b l1' => cons b (append l1' l2)

Now we need induction since we have recursion.

Proof by Induction

Lemma appendToNil: forall l, (append l nil) = l.
Proof.
  intros. induction l as [|b l']. 
  - simpl. reflexivity.
  - simpl. rewrite IHl'. reflexivity.
Qed.
  • induction l means we are carrying induction
  • as [|b l'] is the naming of variables within the sub cases. Kind of like match. each sub case is separated by a bar |.
  • simpl. means execute and simplify. The first case is where l is nil so we get nil. So reflexivity can be applied
  • the second sub case (cons b l')is where induction happens
    • simpl applies def of append and simplify it
    • then we have an induction hypothesis on l', which is
      append l' nil = l'
      we apply this IH by rewrite IHl' which finds all terms in the proof with the LHS and rewrite it with the RHS
    • Lastly we apply reflexivity.

We can also extract the proof as OCaml code as well.

Require Coq.extraction.Extraction.
Extraction Language OCaml.

Extraction append.

Parametric polymorphism

Definition id (T: Type) (x:T) := x.

This is the function of identity. The type is

forall T: Type, T -> T

Type functions are also supported, functions that takes in type and output of type. This gives us polymorphic types.

Module PolyList.

Inductive list (T:Type) : Type :=
  nil : list T
| cons : T -> list T -> list T.
End PolyList.

This is a polymorphic list. We can do integer list, boolean list, whatever list.

We have functions that

  • values -> values
  • types -> values (polymorphic functions)
  • types to types (polymorphic types)

We are missing values -> types. This is called dependent types.

For our convenience, we define the natural number as a type.

Inductive nat : Type :=
  zero : nat
| succ : nat -> nat.

Fixpoint plus(n1 n2 : nat) : nat :=
  match n1 with
    zero => n2
  | succ n1' => succ (plus n1' n2)

Let’s say we want to have a boolean list that keep tracks of its size statically.

Module ListsWithLength.

  Inductive boollist : nat -> Type :=
    nil : boollist zero
  | cons : forall n, bool -> boollist n -> boollist (succ n).

End ListsWithLength.

To use this.

Check (cons (succ zero) false nil).

See how we need to pass 1 (succ zero), which is the length of the list [false].

Note that this is first order logic since cons is forall n, bool

Defining Pairs

Module Pairs.

Inductive prod (A B : Type): Type := 
  pair : A -> B -> prod A B. 

End Pairs.

To build a pair: pair nat bool zero true.

This is built into Gallina. Check with Print prod. However, there is something similar to this.

Print and.

and takes in Prop instead of Type. We have proposition.

Lemma andIsCommutative : forall P Q, and P Q -> and Q P.
Proof.
  intros. inversion H. split.
  - assumption.
  - assumption.
Qed.
  • intros gets us H = and P Q.
  • inversion H create two variables in the inverse order.
  • split separate them into the subcase of Q and P in this order because we applied inversion
  • then for each subcase, we check Q/P is true. By checking our assumption, we know that they are true

Another way of writing this

Definition andIsCommutative2 (P Q : Prop) (H : and P Q) :=
  match H with
    conj HP HQ => conj HQ HP
  end.

This is building a program that is a proof witness.

Dependent Proposition

We had dependent types. Do we have dependent proposition? Yes, they are inference rules.

Let’s have inference rule for even numbers.

isEven zeroIsEvenZero\cfrac{}{ isEven\ zero } \quad IsEven-Zero
isEven nisEven (succ(succ n))IsEvenSuccSucc\cfrac{ isEven\ n }{ isEven\ (succ(succ\ n)) } \quad IsEven-SuccSucc

To express this,

Inductive isEven : nat -> Prop :=
    IsEvenZero : isEven zero
  | IsEvenSuccSucc : forall n, is Even n -> isEven (succ (succ n)).

We proof by constructing a derivation tree:

Definition fourIsEven :=
  IsEvenSuccSucc (succ (succ zero)) (IsEvenSuccSucc zero IsEvenZero).

We can also use proof tactics.

Lemma fourIsEven2 : isEven (succ (succ (succ (succ zero)))).
Proof.
  apply IsEvenSuccSucc. apply IsEvenSuccSucc. apply IsEvenZero.
Qed.

Proofs of 231 in Coq

We express the language of boolean from Chapter 3

Inductive term : Type := 
  t_unit : term (* the Unit type *)
| t_true : term
| t_false : term
| t_if : term -> term -> term -> term.

We can define a simple program if true then false else true as such

t_if t_true t_false t_true.

Now we define the data types.

Inductive ty : Type :=
  Unit : ty
| Bool : ty

Typechecking

Now we define some typechecking rules. We define them as dependent proposition.

Inductive typeof : term -> ty -> Prop := 
| T_True : typeof t_true Bool
| T_False : typeof t_false Bool
| T_Unit : typeof t_unit Unit
| T_If : forall t1 t2 t3 T, typeof t1 Bool -> typeof t2 T -> typeof t3 T -> typeof (t_if t1 t2 t3) T.

We can define simplifying notation in Coq.

Notation "t : T" := (typeof t T) (at level 40).
  • level 40 means the precedence of our new notation.

To derive a derivation tree for simpleProgram:

Definition typeOfSimpleProgram : (typeof simpleProgram Bool) := 
  T_If t_true t_false t_true Bool T_True T_False T_True.
  • t_true t_false t_true Bool is for forall t1 t2 t3 T
  • then we give it a derivation tree for t1 with T_True
  • then we give it a derivation tree for t2 with T_False
  • then we give it a derivation tree for t3 with T_True

We can enforce in teh grammar that an expression has to be well typed.

Module WellTypedExprs.

Inductive term : ty -> Type :=
  t_unit : term Unit
| t_true : term Bool
| t_false : term Bool
| t_if : forall T, term Bool -> term T -> term T -> term T.

End WellTypedExprs.

In the original definition of term, we can define a bad program such as if () then false else true

Definition badProgram := t_if t_unit t_false t_true.

But under the new rule it is no longer valid.

Small-step operational semantics

First we define what is a value.

Inductive isValue : term -> Prop := 
  UnitVal : isValue t_unit
| TrueVal : isValue t_true
| FalseVal : isValue t_false

Define a step relation as a relation between a term and a term:

Inductive step : term -> term -> Prop :=
  E_IfTrue : forall t2 t3, step (t_if t_true t2 t3) t2
| E_IfFalse : forall t2 t3, step (t_if t_false t2 t3) t3
| E_If : forall t1 t2 t3 t1', step t1 t1' -> step (t_if t1 t2 t3) (t_if t1' t2 t3)

Exercise: extending and to the language

Given a new term for and

Inductive term : Type :=
  (* ... *)
| t_and : term -> term -> term

Give a typing rule under typeof.

The solution

Inductive typeof : term -> ty -> Prop := 
  (* ... *)
  T_And : forall t1 t2, typeof t1 Bool -> typeof t2 Bool -> typeof (t_and t1 t2) Bool.

Give a small-step semantics under step.

The solution

Inductive step : term -> term -> Prop := 
  (* ... *)
| E_And1 : forall t1 t2 t1', step t1 t1' -> step (t_and t1 t2) (t_and t1' t2)
(* the rest left as exercise to the reader *)

Proving Progress and Preservation

Notation for stepping

Notation "t1 '-->' t2" := (step t1 t2) (at level 40).
Theorem progress: forall t T, t : T -> isValue t \/ exists t', t --> t'.
Proof.
intros. induction H.
- left. apply TrueVal.
- left. apply FalseVal.
- left. apply UnitVal.
- right. inversion IHtypeof1.
  * apply (canonicalFormsBool t1 H2) in H. inversion H.
    + exists t2. rewrite H3. apply E_IfTrue. 
    + exists t3. rewrite H3. apply E_IfFalse. 
  * inversion H2. exists (t_if x t2 t3). apply E_if. assumption. 
Qed.
  • t : T is from our defined notation
  • intros gives us
    • t is a term
    • T is a ty aka type
    • H: (t: T) is our hypothesis and it is our derivation tree
  • by induction H, we break H into all cases.
    • 1st case is for t_true, so we appeal to TrueVal to the left condition, which is isValue t.
    • 2nd and 3rd case are similar
    • by deriving from the first 3 cases, we get induction hypothesis as extra premises.
    • in the 4th case, if should step, so right.
    • IHtypeof1 is induction hypothesis where t1 can type.
      • if t1 is value of type bool, then it has to be t_true or t_false, but we need the canonical forms for bools. we pause the proof. go and write a lemma before this
        • now
Lemma canonicalFormsBool: forall t, isValue t -> t: Bool -> t = t_true \/ t = t_false.
Proof. 
intros. inversion H. 
- rewrite <- H1 in H0. inversion H0. 
- left. reflexivity.
- right. reflexivity.
  • first case is t_unit, H1: t_unit = t and H0: t:Bool, then we write H1 in H0, then it becomes H0: t_unit : Bool, but this is a contradiction. We call inversion H0. then this is proven

Proof for preservation

Theorem preservation: forall t t' T, t : T -> t --> t' -> t' : T.
Proof.
intros. generalize dependent t'. induction H.
- (* T-True *) intros. inversion H0.
- (* T-False *) intros. inversion H0.
- (* T-Unit *) intros. inversion H0.
- (* T-If *) intros. inversion H2. 
  * (* E-IfTrue *) rewrite <- H3. assumption. 
  * (* E-IfFalse *) rewrite <- H3. assumption. 
  * (* E-If *) apply T_If. 
    + (* t1' is Bool *) apply IHtypeof1. assumption.
    + assumption.
    + assumption. 
Qed.
  • we leave t' as universally quantified using generalize dependent t'
  • first 3 cases are contradiction, H2 is applied.

Note about rewrite

  • In rewrite H, H has to be an equality (A=B), and it rewrites A in the goal with B
  • In rewrite <- H, H has to be an equality (A=B) and it rewrites B in the goal with A
  • In rewrite <- H1 in H2, is like rewrite <- H but instead of rewriting in the goal but it rewrites in the given hypothesis H2

Mutable References

  • book chapter 13

In OCaml,

  • local variables are always immutable
  • all mutable memory is on the heap

In C, int and int* are both mutable In OCaml, int* is represented by the type int ref. int ref is mutable but int is not. This means mutability is explicitly encoded in the type.

(* create a reference *)
ref 0;;

ref (2, "hello");;

ref (function x -> x + 1);;

ref (ref 0);;
  • this creates a reference on the heap with some initial value
(* dereference *)
let r = ref 0;;
!r;;
  • !r is the equivalent of *r in C
(* updating value *)
r := 53;;
(* aliasing *)
let x = ref 0;;
let y = x;;

Once we have reference, we don’t need recursion anymore.

let factorial = ref (function x -> x + 1);;
factorial := 
  function x -> if x = 0 then 1 else x (!factorial)(x-1);;
let fact = !factorial;;
fact(5);;

Reference is really powerful. We can define a cyclic list as well.

type clist = Nil | Cons of int * clist ref;;

let lst = ref Nil;;
lst := Cons(4, lst);;

Object in references

(* an integer counter object *)
let (get, inc, reset) = 
  let c = ref 0 in (
    (function() -> !c), 
    (function() -> c := !c + 1), 
    (function() -> c := 0)
  );;
  
(* make a constructor *)
let makeCounter () = 
  let c = ref 0 in (
    (function() -> !c), 
    (function() -> c := !c + 1), 
    (function() -> c := 0)
  );;

let counter1 = makeCounter();;
let (get1, inc1, reset1) = counter1;;

There is no null reference since all reference has to contain some value. The way to hack up null is union type.

type optionalInt = Null | I of int;;

This is available within the standard library. It is called option.

Typing rules

t := ... | ref t | !t | t := t
T := ... | Ref T
Γt:TΓref t:Ref TTRef\cfrac{ \Gamma\vdash t:T }{\Gamma \vdash \texttt{ref t}: \texttt{Ref }T} \quad T_{Ref}
Γt:Ref TΓ!t:TTDeref\cfrac{ \Gamma\vdash t:\texttt{Ref } T }{ \Gamma\vdash\texttt{!t}: T } \quad T_{Deref}
Γt1:Ref Tt2:TΓt1:=t2:UnitTAssign\cfrac{ \Gamma\vdash t_1: \texttt{Ref }T \quad t_2: T }{\Gamma\vdash t_1:=t_2: Unit} T_{Assign}

Small-step semantics

What are the new value forms? Can we use ref v as a value form? No since there is aliasing.

let r1 = ref 0 in 
let r2 = r1 in 
  (r1 := 4); !r2

r2 copies r1 and gets ref 0 but we can’t propagate the change in r1 := 4 to r2 in our given semantics.

To do this, we introduce new meta-variable

l ::= infinite set of memory locations

A heap/store is a mapping μ\mu from memory location to their values.

New judgement of the form t,μt,μ\lang t, \mu\rang\rightarrow\lang t',\mu'\rang.

t,μt’,μref t,μref t’,μERef\cfrac{ \lang\texttt{t},\mu\rang \rightarrow \lang\texttt{t'},\mu'\rang }{ \lang\texttt{ref t},\mu\rang \rightarrow \lang\texttt{ref t'},\mu'\rang } \quad E_{Ref}
l that is not in domain(μ)ref v,μl,μ[lv]ERefLoc\cfrac{ l \text{ that is not in domain($\mu$)} }{ \lang\texttt{ref v},\mu\rang \rightarrow \lang\texttt{l},\mu[l\mapsto v]\rang } \quad E_{RefLoc}
t,μt’,μ!t,μ!t’,μEDeref\cfrac{ \lang\texttt{t},\mu\rang \rightarrow \lang\texttt{t'},\mu'\rang }{ \lang\texttt{!t}, \mu\rang \rightarrow \lang\texttt{!t'}, \mu'\rang } \quad E_{Deref}
μ(l)=v!l,μv,μEDeRefLoc\cfrac{ \mu(l) = v }{ \lang!l,\mu\rang \rightarrow \lang v,\mu\rang } \quad E_{DeRefLoc}
t1,μt1,μt1:=t2,μt1:=t2,μEAssign1\cfrac{ \lang t_1,\mu\rang \rightarrow \lang t_1',\mu'\rang }{ \lang t_1:=t_2,\mu\rang \rightarrow \lang t_1':=t_2,\mu'\rang } \quad E_{Assign1}
t2,μt2,μv1:=t2,μv1:=t2,μEAssign2\cfrac{ \lang t_2,\mu\rang \rightarrow \lang t_2',\mu'\rang }{ \lang v_1:=t_2,\mu\rang \rightarrow \lang v_1:=t_2',\mu'\rang } \quad E_{Assign2}
l is in the domain(μ)l:=v,μ(),μ[lv]EAssignLoc\cfrac{ l \text{ is in the domain($\mu$)} }{ \lang l:=v, \mu\rang \rightarrow \lang (), \mu[l\mapsto v]\rang } \quad E_{AssignLoc}

Type Soundness with reference

Original type soundness: If {}t:T\{\}\vdash t:T and ttt\rightarrow^* t', then either tt' is a value or there exists tt'' such that ttt'\rightarrow t''.

New type soundness: If {}t:T\{\}\vdash t:T and t,{}tμ\lang t,\{\}\rang\rightarrow^* \lang t'\mu'\rang, then either tt' is a value or there exists tt'' and μ\mu'' such that t,μt,μ\lang t',\mu'\rang\rightarrow \lang t'', \mu''\rang.

New Progress:

If {}t:T\{\}\vdash t:T, then either tt is a value or there exists tt' and μ\mu' such that t,μt,μ\lang t,\mu \rang\rightarrow \lang t',\mu'\rang.

  • something is wrong about this, where is μ\mu coming from?
  • how do we guarantee that we don’t do invalid reference!

New Preservation:

If {}t:T\{\}\vdash t:T and t,μtμ\lang t,\mu \rang\rightarrow \lang t'\mu'\rang, then {}t:T\{\}\vdash t': T.

Typing rule of location

Problem: In our small step semantics, we introduce some intermediate of memory location l. We can step to some term containing memory location. To prove this in preservation/progress, we need typing rules for l.

Example: preservation does not currently hold.

  • {}ref 0:Ref Int\{\}\vdash\texttt{ref 0}:\texttt{Ref }Int
  • and ref 0,{}l,{(l,0)}\lang \texttt{ref 0}, \{\}\rang \rightarrow \lang l, \{(l, 0)\}\rang
  • but ll is not well-typed.
Γl:??\cfrac{}{ \Gamma\vdash l : ?? }

We don’t know what is the type of the thing we store in a memory location. We introduce a new mapping that keeps track of types of the stuff we stored in memory.

Introduce a store typing Σ\Sigma. The typing judgement is of the form Γ;Σt:T\Gamma;\Sigma\vdash t:T. Therefore, the typing rule for location is

Σ(l)=TΓ;Σl:Ref TTLoc\cfrac{ \Sigma(l) = T }{ \Gamma;\Sigma \vdash l : \texttt{Ref }T } \quad T_{Loc}

Let’s update Progress/Preservation to account for Σ\Sigma:

  • Progress: If {};St:T\{\};S\vdash t:T, then either tt is a value or there exists tt' and μ\mu' such that t,μt,μ\lang t,\mu\rang \rightarrow \lang t',\mu'\rang.
  • Preservation: If {};St:T\{\};S\vdash t:T and t,μt,μ\lang t,\mu\rang\rightarrow \lang t',\mu'\rang, then {};St:T\{\};S\vdash t':T.

But this is still incorrect, consider the following example.

!l
  • {};{(l,Int)}!l:Int\{\};\{(l,Int)\}\vdash !l : Int but !l,{}\lang !l,\{\}\rang doesn’t step. Contradiction to Progress.
  • {};{(l,Int)}!l+1:Int\{\};\{(l,Int)\}\vdash !l + 1: Int, and !l+1,{(l,true)}true+1,{(l,true)}\lang !l+1,\{(l,true)\}\rang\rightarrow \lang true + 1, \{(l, true)\}\rang but true+1true + 1 is not well-typed. Contradiction to Preservation

Problem: there is no connection between Σ\Sigma and μ\mu.

We introduce a new relationship.

Definition (Heap typing): We say Σ\Sigma and μ\mu are consistent (denoted Σμ\Sigma\vdash\mu) if:

  • domain(Σ)=domain(μ)domain(\Sigma)=domain(\mu)
  • ldomain(Σ),{};Σμ(l):Σ(l)\forall l \in domain(\Sigma), \{\};\Sigma\vdash \mu(l) : \Sigma(l)

Now update Progress/Preservation:

  • Progress: If {};Σt:T\{\};\Sigma\vdash t:T and Σμ\Sigma\vdash\mu, then either tt is a value or there exists tt' and μ\mu' such that t,μt,μ\lang t,\mu\rang \rightarrow \lang t',\mu'\rang.
  • Preservation: If {};Σt:T\{\};\Sigma\vdash t:T and Σμ\Sigma\vdash\mu and t,μt,μ\lang t,\mu\rang\rightarrow \lang t',\mu'\rang, then there exists an Σ\Sigma' such that {};Σt:T\{\};\Sigma'\vdash t':T and Σμ\Sigma'\vdash \mu'.

Subtyping

Introduce a subtyping relation, denoted S<:TS<: T

  • “S is more specific than T”
  • “values of type S can be passed where values of type T are expected”
    • principle of subtype substitutability

We would like to do this in our language

let f = function x -> if (fst x) then (snd x) + 1 else (snd x) - 1
in
f (true, 1, "some other info")
  • OCaml doesn’t let us do this
  • but it should be allowed since extracting first and second from a triple containing extra info should be a subtype
  • conceptually a tuple is a struct/record, to extend a struct/record we add extra fields

Formalize the principle of subtype substitutability through a typing rule called “subsumption”:

Γt:SS<:TΓt:TTSub\cfrac{ \Gamma \vdash t: S \quad S <: T }{ \Gamma \vdash t: T } \quad T_{Sub}
  • if tt is type SS and SS is a subtype of TT, then we can treat tt as type TT.

Next we define the subtyping relationship <:<:.

Consider the simply-typed lambda calculus plus base types (ints and bools) plus (arbitrary-length) tuples.

T ::= Int | Bool | T -> T | ... | T * T * ... * T | Top
  • we introduce a Top type
  • This is the equivalence of the root type Object in Java
T<:TSRefl\cfrac{}{ T <: T } \quad S_{Refl}
S<:UU<:TS<:TSTrans\cfrac{ S<: U\quad U <:T }{ S <: T } \quad S_{Trans}
T<:TopSTop\cfrac{ }{ T <: Top } \quad S_{Top}
S1<:T1S2<:T2...Sm<:TmS1Sm<:T1TmSDepthTuple\cfrac{ S_1 <: T_1 \quad S_2 <: T_2 \quad ... \quad S_m <: T_m \quad }{ S_1 * \dots * S_m <: T_1 * \cdots T_m } \quad S_{DepthTuple}
  • e.g. Int * Bool <:<: Top * Bool
mnT1Tm<:T1TnSWidthTuple\cfrac{ m \geq n }{ T_1 * \dots * T_m <: T_1 * \dots * T_n } S_{WidthTuple}
  • e.g. Bool * Int * Bool <:<: Bool * Int

Combining SDepthTupleS_{DepthTuple} and SWidthTupleS_{WidthTuple}, we can have subtyping for shorter tuples with subtypes.

Now define subtyping for functions?

T1<:S1S2<:T2S1S2<:T1T2SFun\cfrac{ T_1 <: S_1 \quad S_2 <: T_2 }{ S_1 \rightarrow S_2 <: T_1 \rightarrow T_2 } \quad S_{Fun}

Consider a formal parameter f of type T1 -> Int * Int * Int. Can we pass in for f a value of type:

  • T1 -> Int * Int? (No)
  • T1 -> Int * Int * Int * Int? (Yes)

This means S2<:T2S_2 <: T_2.

Consider a formal parameter f of type Int * Int * Int -> T2. Can we pass in for f a value of type:

  • Int * Int -> T2? (Yes)
  • Int * Int * Int * Int -> T2? (No)

This means T1<:S1T_1 <: S_1.

Example in Java:

class C {
  Object m() {
    return new Integer(4);
  }
}

class D extends C {
  String m() {
    return "hello";
  }
}
  • see how the subtype function m is valid since String <: Object

Suppose we add mutable references Ref T to our language

Can we do this?

Ref S<:Ref TSRef\cfrac{}{ \texttt{Ref S} <: \texttt{Ref T} } \quad S_{Ref}
  • Ref Int * Int <: Ref Int * Int * Int? NOT OK.
  • Ref Int * Int * Int <: Ref Int * Int?
    • can we do this?
      let (r: Ref Int*Int*Int) = ref (0, 1, 2) in 
      let (s: Ref Int*Int) = r in
        (* ... *)
      No! what if we
      s := (0, 1)
      This is dangerous since s is an alias. Then we…
      (fst !r) + (trd !r)
      (where trd is taking a third element)

We cannot allow subtyping for references!

Suppose that S <: T.

  • Can we consider Ref T <: Ref S?
    • can Ref Top <: Ref Int?
    • no
      let (r: Ref Top) = ref true in
      let (s: Ref Int) = r in 
      !s + 5
  • Can we consider Ref S <: Ref T?
    • can Ref Int <: Ref Top?
    • no
      let (r: Ref Int) = ref 0 in
      let (s: Ref Top) = r in 
      s := true; !s + 1

In Java, the problem comes up in the following form

class C {
  Object f;
  void m() {
    f = new Integer(34);
  }
}

class D extends C {
  override String f; // not legal Java
  int n() {
    return f.length;
  }
}

Since someone can assign int to f, then override String f has to be illegal otherwise it is unsafe.

Now consider List. List<String> is not a subtype of List<Object> because List are mutable.

class Test {
  void m(List<Object> l) {
    // we can append an integer to a list of string
    l.add(new Integer(34)); 
  }
}

class Main {
  public static void main(String[] args) {
    Test t = new Test();
    
    List<String> strs = new LinkedList<String>();
    strs.add("hi");
    strs.add("there");
    t.m(strs); // this will upset the compiler 
  }
}

We can append integer to a list of strings. Java doesn’t allow that.

But what if we want

void printAll(List<Object> l) {
  for (Object o:l) { 
    System.out.println(o);
  }
}

In this case, it is completely safe. But the type system is stopping us. We need parametric polymorphism

<T> void printAll(List<T> l) {
  for (T o:l) { 
    System.out.println(o);
  }
}

Consider Array. The same problem happens, however…

class Test {
  void m2(Object[] l) {
    l[0] = 1;
  }
}

class Main {
  public static void main(String[] args) {
    Test t = new Test();
    
    t.m2(args); 
  }
}

This will typecheck! But it will crash at runtime. Historical baggage or tech debt. In early days of Java there is no Generics, to make the language pragmatic. They let this happen in compile time but trade it by giving up runtime performance and runtime safety.

Consider this example. We have some Shape that we can draw.

interface Shape {
  void draw();
}

class Rectangle implements Shape {
  void draw() { /* ... */ }
}

class Test {
  void drawAll(List<Shape> l) {
    for (Shape s : l) {
      s.draw();
    }
  }
}

class Main {
  public static void main(String []args) {
    List<Rectangle> rects = new LinkedList<Rectangle>();
    t.drawAll(rects); // compiler complains
  }
}

This is doesn’t compile for the same reason. Can we use parametric polymorphism?

<T> void drawAll(List<T> l) {
  for (T s : l) {
    s.draw();
  }
}

This also doesn’t compile. Since T doesn’t have a draw method. However, we can use bounded parametric polymorphism.

<T extends Shape> void drawAll(List<T> l) {
  for (T s : l) {
    s.draw();
  }
}

Now this type checks.

Type and Effect Systems

Side effects are not visible in the type of the functions.

let r = ref 42;;
let inc = function x ->  x + 1;;
let inc2 = function x -> r := 1000; x + 1;;

Both inc and inc2 has type int -> int. However, inc2 has side effects but we don’t know from its type.

What doesn’t function type tell us?

  • does the function read/write mutable memory?
  • does the function read/write to a file?
  • does the function send/receive packets?
  • does the function acquire/release lock?

There are a lot of things that the types don’t tell us about.

A type and effect system then is a system that also tracks side effects.

Simple idea: augment the type system to track a set of “effects”

ϕ ::= {} | { read } | { write } | { alloc } | ϕ U ϕ

New typing judgment:

Γ;Σt:T;ϕ\Gamma;\Sigma \vdash t : T ; \phi

tt has type TT and tt’s evaluation may cause the effects ϕ\phi

Example: Γ;Σt:T;{}\Gamma;\Sigma\vdash t: T ; \{\} implies the term is pure.

  • this means we can cache calls

Example: Γ;Σt:T;{read}\Gamma ;\Sigma\vdash t: T ; \{ read \} implies the tt is side-effect-free but does not change the heap.

  • this means there is no side effect but the calls cannot be cached

Now consider the rules for references.

Γ;Σt:T;ϕΓ;Σref t:Ref T;ϕ{alloc}TRef\cfrac{ \Gamma;\Sigma\vdash t : T;\phi }{ \Gamma;\Sigma\vdash\texttt{ref t} : \texttt{Ref T}; \phi\cup\{alloc\} } \quad T_{Ref}
Γ;Σt:Ref T;ϕΓ;Σ!t:T;ϕ{read}TDeref\cfrac{ \Gamma;\Sigma\vdash t : \texttt{Ref } T;\phi }{ \Gamma;\Sigma\vdash\texttt{!t} : T; \phi\cup\{read\} } \quad T_{Deref}
Γ;Σt1:Ref T;ϕ1Γ;Σt2:T;ϕ2Γ;Σt1:=t2:Unit;ϕ1ϕ2{write}TAssign\cfrac{ \Gamma;\Sigma\vdash t_1 : \texttt{Ref }T;\phi_1 \quad \Gamma;\Sigma\vdash t_2 : T;\phi_2 }{ \Gamma;\Sigma\vdash t_1 := t_2 : Unit; \phi_1\cup\phi_2\cup\{write\} } \quad T_{Assign}

Now, we can tell the difference between 0 and !(ref 0).

  • Γ;Σ!(ref 0):Int;{alloc,read}\Gamma;\Sigma\vdash !(\texttt{ref } 0) : Int;\{alloc, read\}
  • Γ;Σ0:Int;{}\Gamma;\Sigma\vdash 0 : Int;\{\}

Let’s augment the rest of the typing rules.

Γ;Σtrue:Bool;{}TTrue\cfrac{}{ \Gamma;\Sigma\vdash true : Bool ; \{\} } \quad T_{True}
Γ;Σfalse:Bool;{}TFalse\cfrac{}{ \Gamma;\Sigma\vdash false : Bool ; \{\} } \quad T_{False}
Γ;Σt1:Bool;ϕ1Γ;Σt2:T;ϕ2Γ;Σt3:T;ϕ3Γ;Σif t1 then t2 else t3:T;ϕ1ϕ2ϕ3TIf\cfrac{ \Gamma;\Sigma\vdash t1 : Bool ;\phi_1 \quad \Gamma;\Sigma\vdash t2 : T ;\phi_2 \quad \Gamma;\Sigma\vdash t3 : T;\phi_3 }{ \Gamma;\Sigma\vdash \texttt{if t1 then t2 else t3} : T ; \phi_1\cup\phi_2\cup\phi_3 }\quad T_{If}

From the case of if, we see that our system is an over-approximation and conservative since we don’t know which branch it is going to take.

Final

  • no polymorphic lambda calculus, no existential types
  • but include let polymorphism

Continuing on effect system

Consider let x = ...

Γ;Σt1:T1;ϕ1Γ,x:T1;Σt2:T;ϕ2Γ;Σlet x = t1 in t2:T;ϕ1ϕ2TLet\cfrac{ \Gamma;\Sigma\vdash t1: T1;\phi_1 \quad \Gamma,x:T1;\Sigma\vdash t2: T;\phi_2 }{\Gamma;\Sigma\vdash\texttt{let x = t1 in t2}: T ; \phi_1\cup\phi_2} \quad T_{Let}

Consider variable look up

Γ(x)=TΓ;Σx:T;{}TVar\cfrac{\Gamma(x)=T}{\Gamma;\Sigma\vdash x : T ; \{\}} \quad T_{Var}

Consider declaring a function

Γ,x:T1;Σt2:T2;ϕΓ;Σfunction x:T1 -> t2:T1T2;{}TFunc\cfrac{ \Gamma,x:T1;\Sigma\vdash t2: T2;\phi }{ \Gamma;\Sigma\vdash\texttt{function x:T1 -> t2}: T1\rightarrow T2;\{\} } \quad T_{Func}
  • declaring a function doesn’t have any effect
  • but we are ignoring them on the floor…

Consider calling a function

Γ;Σt1:T2T;ϕ1Γ;Σt2:T2;ϕ2Γ;Σt1 t2:T;ϕ1ϕ2TApp\cfrac{ \Gamma;\Sigma\vdash t1 : T2 \rightarrow T ; \phi_1 \quad \Gamma;\Sigma\vdash t2 : T2 ; \phi_2 }{ \Gamma;\Sigma\vdash\texttt{t1 t2}: T ; \phi_1\cup\phi_2 } \quad T_{App}
  • but now, the side effect in t1 is not accounted for!
  • our type system is unsound

Solution to unsoundness: augment function type to also keep track of the body of the function.

T1ϕT2T_1 \xrightarrow{\phi}T_2
Γ,x:T1;Σt2:T2;ϕΓ;Σfunction x:T1 -> t2:T1ϕT2;{}TFunc\cfrac{ \Gamma,x:T1;\Sigma\vdash t2: T2;\phi }{ \Gamma;\Sigma\vdash\texttt{function x:T1 -> t2}: T1\xrightarrow{\phi} T2;\{\} } \quad T_{Func}
Γ;Σt1:T2ϕT;ϕ1Γ;Σt2:T2;ϕ2Γ;Σt1 t2:T;ϕϕ1ϕ2TApp\cfrac{ \Gamma;\Sigma\vdash t1 : T2 \xrightarrow{\phi} T ; \phi_1 \quad \Gamma;\Sigma\vdash t2 : T2 ; \phi_2 }{ \Gamma;\Sigma\vdash\texttt{t1 t2}: T ; \phi\cup\phi_1\cup\phi_2 } \quad T_{App}

The effect is now part of the type language.

Java has an effect system. It is called exception.

class Exn1 extends Exception {}
class Exn2 extends Exception {}

class Test {
  void m1(int x) throws Exn1 {
    if (x < 0) throw new Exn1();
    // ...
  }
  
  void m2(int y) throws Exn1, Exn2 {
    m1(y);
    if (y > 100) throw new Exn2();
  }
  
  void m3(int z) throws Exn2 {
    try {
      m2(z);
    } catch (Exn1 e) {
      // ...
    }
  }
}
  • throws documents the exception side effect.
  • we “consume” the effect by using try-catch

Monads

There are 2 distinct aspect to monads

  • ability to track effects of various kinds in the type system
  • a cool programming pattern for separating out code that manipulates effects from the “normal” code does the regular computation
let h x y = x/y

let g x = x + 1

let f x = x * 2 

let myprogram x y = 
  let v = h x y in 
  let u = g v in 
  f u

h can throw a division by 0 exception. We can wrap it in an option type.

let h' x y = if y = 0 then None else Some (x / y)

However, we cannot just replace h in myprogram with h' since now it returns an option. Then this bubbles up all the way in the program. So much pattern matching to do!

Problem: so much boilerplate code that bubbles up everywhere.

A monad is an API that allows all the boilerplate handling of the effect to be modularized. Two functions:

  • return: a function that lifts a value into monad (a' -> a' option)
  • bind: thread an effect through a computation ('a option -> ('a -> 'b option) -> b' option)
    • give me some result with an effect, a function to handle a value and have some effect. Give you back a combined effect.
let return x = Some x;;
(* defining an infix operator for bind *)
let (>>=) x f =
  match x with 
    None -> None
  | Some x' -> (f x')

Now re-consider myprogram

let myprogram' x y =
  (h' x y) >>= (function v -> return (g v)) >>= (function u -> return (f u))

Consider another example. This time, we want to log all function execution. To capture the side effect, we have a type that capture the side effect.

type 'a logged = 'a * string

Then we define return and bind

let return x = (x, "")

let (>>=) x f = 
  let (v, log) = x in
  let (v', log') = f v in 
  (v', log ^ " " ^ log')

Now modify my program:

let h x y = (x/y, "h")

let g x = (x + 1, "g")

let f x = (x * 2, "f")

let myprogram' x y = 
  (h x y) >>= g >>= h

Each function only cares about their own logs. The concatenation is abstracted away from them.