In the previous post, we introduced Hatsugen, a small programming language with integer and boolean types.
In this post, we’ll add functions to Hatsugen. With this addition, Hatsugen becomes approximately as powerful as the simply-typed lambda calculus.
We update the expression syntax to add variables, function literals, and function application. We also update the type syntax to add function types.
For variables, we just write things like x or y or x1 or x′. There are infinitely many variable names to choose from.
λ(x:τ) e is a function literal, taking one argument x of type τ and evaluating e when applied to an argument. x may appear in the expression e.
e1(e2) is an application expression, representing the application of the function e1 to the argument e2.
τ1→τ2 is the type of functions taking τ1 as input and returning τ2 as output.
τ::= ∣ e::= ∣ ∣ ∣ …τ1→τ2…xλ(x:τ) ee1(e2)
Consider the expression λ(x:Int) x. It should have type Int→Int.
The very similar expression λ(x:Bool) x should have type Bool→Bool.
Notice that both of these example expressions contain the sub-expression x, as the body of the function. But in each, the type of x is different: Int in the first and Bool in the second.
As this example illustrates, the type of variables is determined by how the variable is declared. This is the first time that the same expression (in this case x) can have a different type depending on the context.
We’ll need to fundamentally change the structure of the static semantics to account for variables.
First, we’ll need to formalize the notion of a context. We’ll use Γ to represent a context, which will just be a list of variable-type pairs.
A context can either be empty, or it can be an existing context augmented with a new variable-type pair.
Γ::= ∣ ⋅Γ,x:τThe old typing judgement was e:τ, read “e has type τ”. The new judgement is written Γ⊢e:τ, read “Γ entails e has type τ”.
We’ll need to update all the statics rules from the first post. All of these rules just use the context without changing it.
Γ⊢n:IntΓ⊢true:BoolΓ⊢false:BoolΓ⊢if e1 then e2 else e3:τΓ⊢e1:BoolΓ⊢e2:τΓ⊢e3:τWe can now add the typing rules for the new constructs.
For variables, we need to be able to look up the variable in question in the context to get its type. For that, we’ll define a small helper judgement for context lookup, written Γ(x)=τ.
(Γ,x:τ)(x)=τ(Γ,y:τ′)(x)=τx=yΓ(x)=τNote that these rules engender shadowing, which is where multiple variable-type pairs with the same variable exist in the context at once, but the one furthest to the right in the context determines the type of the variable under the context. For instance, using the rules we can derive
(⋅,x:Int,x:Bool,y:Int)(x)=BoolWe can now use this helper lookup judgement in the typing rule for variables.
Γ⊢x:τΓ(x)=τFor function literals, the type of the parameter is the input type, and the type of the function body is the output type.
However, the bound variable may appear in the function body. So we add the variable and its type to the context when determining the function body’s type.
Γ⊢λ(x:τ1) e:τ1→τ2Γ,x:τ1⊢e:τ2For application, the parameter and argument types must match.
Γ⊢e1(e2):τ2Γ⊢e1:τ1→τ2Γ⊢e2:τ1
Functions are values, regardless of whether the function body is a value:
λ(x:τ) e valFor application expressions, we first step the function expression. Then, once it’s a value, we step the argument to a value as well:
e1(e2)↦e1′(e2)e1↦e1′e1(e2)↦e1(e2′)e1 vale2↦e2′Once both expressions are values, we can prove the first will be a function literal, since it had function type.
We will step into the body of the function, substituting all free occurrences of the variable bound by the function with the value of the argument. We write [x↦ex]e=e′ to mean “substituting all free occurrences of x with ex in e yields e′”.
(λ(x:τ) e) e2↦e′e2 val[x↦e2]e=e′
To define the dynamics for function application, we must now define substitution for expressions.
Substitution does nothing to integer and boolean literals:
[x↦ex]n=n[x↦ex]true=true[x↦ex]false=falseFor conditional and application expressions, we simply recurse on the sub-expressions:
[x↦ex]if e1 then e2 else e3=if e1′ then e2′ else e3′[x↦ex]e1=e1′[x↦ex]e2=e2′[x↦ex]e3=e3′[x↦ex]e1(e2)=e1′(e2′)[x↦ex]e1=e1′[x↦ex]e2=e2′For variables, we case on whether the variable in the expression is the variable being substituted. If it is, we replace the variable with ex. If not, we leave it alone.
[x↦ex]x=ex[x↦ex]y=yx=yFor function literals, we again case on whether the variables are the same. If they are, we leave the function literal untouched. This is consistent with how we treat variables with the same name in the context Γ.
[x↦ex]λ(x:τ) e=λ(x:τ) eFor the case when the variables are different, we must take care to avoid variable capture. For example, if we define
[x↦ex]λ(y:τ) e=λ(y:τ) e′x=y[x↦ex]e=e′we can use the rules to prove that
[x↦y]λ(y:Bool) x=λ(y:Bool) yIn this case, the variable y has been captured by the binding for y in the function literal.
To avoid this, we require that the variable bound by the function literal not appear free in ex. We thus revise the rule, writing fv(ex) to denote the free variables in ex.
[x↦ex]λ(y:τ) e=λ(y:τ) e′x=yy∈/fv(ex)[x↦ex]e=e′
We must now define the free variables of an expression.
Integer and boolean literals have no free variables:
fv(n)=∅fv(true)=∅fv(false)=∅For conditional and application expressions, we simply recurse and union:
fv(if e1 then e2 else e3)=s1∪s2∪s3fv(e1)=s1fv(e2)=s2fv(e3)=s3fv(e1(e2))=s1∪s2fv(e1)=s1fv(e2)=s2Variables alone are free:
fv(x)={x}Function literals bind a single variable:
fv(λ(x:τ) e)=s∖{x}fv(e)=s
Since the judgments have changed a bit, we’ll need to restate the theorems slightly.
For all Γ and e and τ, if Γ⊢e:τ and fv(e)=∅, then e val or there exists e′ such that e↦e′.
We require that e have no free variables at all. This will allow us to perform substitution as we evaluate.
To see why this is necessary, consider choosing e to be
(λ(x:Bool→Bool) x) (λ(y:Bool) x)This expression has one free variable, x. According to the rules, this expression is neither a value nor can it step. This is because x appears free in the argument but is bound by the function being applied.
The expression x is an even smaller counterexample, since bare variables are neither values nor can they step. Variables are given meaning by substitution.
For all Γ and e and e′ and τ, if Γ⊢e:τ and fv(e)=∅ and e↦e′, then Γ⊢e′:τ and fv(e′)=∅.
Preservation not only preserves typing, but also the presence of free variables. This is important, since we need there to be no free variables in e′ in order to be able to feed e′ back into the progress theorem.
Once again, taken together we have the following safety theorem:
For all Γ and e and τ, if Γ⊢e:τ and fv(e)=∅, then e val or there exists e′ such that e↦e′ and Γ⊢e′:τ and fv(e′)=∅.
The proofs are once again on GitHub.
In the next post, we’ll add product types, also known as structs or tuples.