Simple and Inductive Types

The Natural Numbers Type


Learning Objectives

  • You know how case matching and the recursor for natural numbers are typed.
  • You know how to implement dynamics for case matching and the recursor with call-by-value semantics.

Introduction

In this chapter we extend simply typed lambda calculus with a natural numbers type. The process is very similar as what we did in Extensions to Lambda Calculus, but this time we also discuss how to infer the types of these terms. We define dynamics for the natural number terms using the call-by-value (CBV) reduction rules instead of full 𝛽-reduction.

We start by extending the set of terms with the zero and successor terms for now — the recursor and pattern matching are added a bit later.

NotationWe use 𝑍 to denote the zero term and 𝑆(𝑡1) to denote the successorof 𝑡1.

The dynamics of zero and successor are the usual ones — no reduction rule for zero, and the Succ1-rule reduces the inner term of a successor.

𝑡CBV𝑡Succ1𝑆(𝑡)CBV𝑆(𝑡)

On the statics side, we extend types with a Nat type.

Nattype

We also specify that 𝑍 is a value and 𝑆(𝑡) is a value whenever 𝑡 is a value.

DefinitionValue (Natural Numbers)𝑍 is a_value and 𝑆(𝑡) is a value if 𝑡 is a value.

Typing Rules

The (hypothetical) typing rules for zero and successor terms are straight-forward. A zero term always (i.e. in any context) has type Nat, whereas the 𝑆(𝑡1) is a Nat only if 𝑡1 has type Nat in the same context. This is expressed using the following rules:

ZeroΓ𝑍::NatΓ𝑡1::NatSuccΓ𝑆(𝑡1)::Nat
Loading Exercise...

Matching Cases

Next, we add a term for matching a natural number using two cases: a zero case and a successor case. The successor case will be expressed as an abstraction which takes in as argument the predecessor of the scrutinee. This avoids the complexity of adding a new variable-binding construction to the language.

DefinitionNatural Number Case Analysis TermGiven terms 𝑡1,𝑡𝑧,𝑡𝑠, the case analysis term for natural numbers isdenoted ncase(𝑡1,𝑡𝑧,𝑡𝑠). NotationNatural Number Case Analysis TermUsually, when working with ncase(𝑡1,𝑡𝑧,𝑡𝑠), 𝑡𝑠 is an abstraction 𝑡𝑠=𝜆𝑝:Nat.𝑡𝑝 for some term 𝑡𝑝. We can write the term asncase(𝑡1,𝑡𝑧,𝑝.𝑡𝑝) with the 𝜆 omitted for clarity.This notation has a more direct connection to the usual case-ofconstruct, which represents the same term.ncase𝑡1of|𝑍𝑡𝑧|𝑆(𝑝)𝑡𝑝

The dynamics of case analysis are as follows. To reduce ncase(𝑡1,𝑡𝑧,𝑡𝑠), we can reduce any subterm 𝑡1,𝑡𝑧,𝑡𝑠 using the following rules

𝑡1CBV𝑡1CaseNat1ncase(𝑡1,𝑡𝑧,𝑡𝑠)CBVncase(𝑡1,𝑡𝑧,𝑡𝑠)

The reduction rules for the root ncase term are as follows:

  • If 𝑡1 is the zero term 𝑍, we take the “zero branch”:

    CaseNatZncase(𝑍,𝑡𝑧,𝑡𝑠)CBV𝑡𝑧
  • If 𝑡1 is the successor term 𝑆(𝑣), we take the “successor branch”. The idea is that the term 𝑡𝑠 corresponding to the successor branch should be a function, so we can simply apply it to 𝑣.

    𝑣valueCaseNatSncase(𝑆(𝑣),𝑡𝑧,𝑡𝑠)CBV𝑡𝑠𝑣

Here, the CaseNatS rule reduces the ncase to an application 𝑡𝑠𝑣, which when reduced further, should1 perform a substitution when we have an application of an abstraction.

Some authors define ncase as a variable-binding construct and define the dynamics of the successor branch using a direct substitution. Instead, we avoid the complexity of introducing another variable-binding construct, as it is just simpler to have 𝑡𝑠 be an abstraction.

Alternative Dynamics

If we wanted to give dynamics directly for ncase(𝑡1,𝑡𝑧,𝑝.𝑡𝑝), we could do so, and it would avoid an unnecessary AppAbs step.

𝑣valueCaseNatSncase(𝑆(𝑣),𝑡𝑧,𝑝.𝑡𝑝)CBV[𝑝𝑣]𝑡𝑝

Notice that 𝑝.𝑡𝑝 is in reality the abstraction 𝜆𝑝:Nat.𝑡𝑝.

The CaseNatS rule presented earlier is a bit easier to implement in Rust, whereas the above CaseNatS' reduces the need for doing an AppAbs step later. However, because the successor branch may not always be an abstraction, we would need to add another reduction rule CaseNat3.

The typing rules of ncase can be derived from the familiar notion of case matching. The term ncase(𝑡1,𝑡𝑧,𝑝.𝑡𝑝) can be expressed as case analysis with two branches:

ncase𝑡1of|𝑍𝑡𝑧|𝑆(𝑝)𝑡𝑝

To infer the type of ncase(𝑡1,𝑡𝑧,𝑝.𝑡𝑝), the type of the “zero branch” must match the “successor branch”. This means that if 𝑡𝑧 has type 𝑇 in the context Γ, then 𝑡𝑝 must have type 𝑇 in the context Γ,𝑝::Nat, i.e. Γ,𝑝::Nat𝑡𝑝::𝑇. This requirement for the successor branch can be equivalently stated as

Γ𝜆𝑝:Nat.𝑡𝑝::Nat𝑇

So the typing rule CaseNat, where 𝑡𝑠 stands for the successor branch term (e.g. 𝜆𝑝:Nat.𝑡𝑝), is written as

Γ𝑡1::NatΓ𝑡𝑧::𝑇Γ𝑡𝑠::Nat𝑇CaseNatΓncase(𝑡1,𝑡𝑧,𝑡𝑠)::𝑇

Inversion of Typing Rules

Let’s provide a type theoretic justification for the equivalence stated above.

Feel free to skip the proof on a first reading.

TheoremThe term 𝑡𝑝 has type 𝑇 in the context Γ,𝑝::Nat if and only if 𝜆𝑝:Nat.𝑡𝑝 has type Nat𝑇 in the context Γ.Proof: The following instance of the Abs rule shows the “only if” () -directionΓ,𝑝::Nat𝑡𝑝::𝑇AbsΓ𝜆𝑝:Nat.𝑡𝑝::Nat𝑇Now, the proof of the “if” () -direction. Suppose Γ𝜆𝑝:Nat.𝑡𝑝::Nat𝑇is the result of a valid typing derivation Δ. Since the typing rules are definedinductively, we can use induction (or case analysis) on Δ.First, let’s consider the Var rule: assume that Δ (with conclusion Γ𝜆𝑝:Nat.𝑡𝑝::Nat𝑇) is an instance of the Var rule:Δ=VarΓ𝜆𝑝:Nat.𝑡𝑝::Nat𝑇Since the conclusion of Var always has shape Γ𝑥::𝑈, where 𝑥 is a variable,it can not be the conclusion of Δ whose term is a lambda abstraction 𝜆𝑝:Nat.𝑡𝑝, so we must have a contradiction.Next, let’s consider the App rule. We get that Δ is an instance of App:Δ=AppΓ𝜆𝑝:Nat.𝑡𝑝::Nat𝑇And again we have a contradiction as the term in the conclusion of an App ruleis 𝑡1𝑡2 which is never equal to 𝜆𝑝:Nat.𝑡𝑝.Zero, Succ and CaseNat rules are ruled out for the same reason, so the onlyrule whose conclusion matches Γ𝜆𝑝:Nat.𝑡𝑝::Nat𝑇 is the Abs rule.Δ=Γ,𝑝::Nat𝑡𝑝::𝑇AbsΓ𝜆𝑝:Nat.𝑡𝑝::Nat𝑇Therefore, as Δ is a valid derivation, Γ,𝑝::Nat𝑡𝑝::𝑇 must hold, which iswhat we wanted to show.

This property, which in fact applies to every typing rule in the language, is known as inversion of typing rules: because at most one typing rule can be applied to any given term, we always known which typing rule to use by looking at the shape of the term.

The inversion property is important as it can be used to show that terms have a unique type. This is also why it’s straight-forward to infer the type of a term recursively in a deterministic/functional way as we have noticed while implementing type checkers on this course.

Your Turn to Implement

Here is a reference for the reduction and typing rules you need to implement in the following assignment.

Dynamics (𝑡CBV𝑡)𝑡CBV𝑡Succ1𝑆(𝑡)CBV𝑆(𝑡)𝑡1CBV𝑡1CaseNat1ncase(𝑡1,𝑡𝑧,𝑡𝑠)CBVncase(𝑡1,𝑡𝑧,𝑡𝑠)CaseNatZncase(𝑍,𝑡𝑧,𝑡𝑠)CBV𝑡𝑧𝑣valueCaseNatSncase(𝑆(𝑣),𝑡𝑧,𝑡𝑠)CBV𝑡𝑠𝑣Typing (Γ𝑡::𝑇)ZeroΓ𝑍::NatΓ𝑡1::NatSuccΓ𝑆(𝑡1)::NatΓ𝑡1::NatΓ𝑡𝑧::𝑇Γ𝑡𝑠::Nat𝑇CaseNatΓncase(𝑡1,𝑡𝑧,𝑡𝑠)::𝑇
Loading Exercise...

Recursion

As natural numbers are a recursive data type, they are not very useful without recursion. Unfortunately, the fixed-point or Y combinator in untyped lambda calculus is not typeable in the simply typed lambda calculus2, i.e. no types can be provided for the variables bound in the abstractions:

𝑌𝜆𝑔.(𝜆𝑥.𝑔(𝑥𝑥))(𝜆𝑥.𝑔(𝑥𝑥))

To show this, we begin by observing that even 𝜆𝑥:𝑇.𝑥𝑥 is ill-typed, i.e. does not have a type in the empty context.

Proposition𝜆𝑥:𝑇.𝑥𝑥 is ill-typed.Proof: By way of contradiction, assume that 𝜆𝑥:𝑇.𝑥𝑥 is well-typed for sometype 𝑇. As the term is an abstraction, according to inversion of typing rules,the only rule that applies is Abs. Likewise, because 𝑥𝑥 is an application, itmust be the conclusion of the App rule, and so we get a derivation withunknown types 𝑈,𝑉:𝑥::𝑇𝑥::𝑉𝑈𝑥::𝑇𝑥::𝑉App𝑥::𝑇𝑥𝑥::𝑈Abs𝜆𝑥:𝑇.𝑥𝑥::𝑇𝑈Using the inversion property of the Var rule on the first premise, we must havethat 𝑇=𝑉𝑈. However, from the second premise, we must have that 𝑇=𝑈, so we get 𝑇=𝑉𝑇 which when recursively expanded yields an infinitearrow type𝑇=𝑉𝑉𝑉However, as types are defined inductively, they must be finite, so we musthave a contradiction.

This is exactly the logic behind why 𝜆𝑥.𝑔(𝑥𝑥) can’t be typed, and as a consequence, the Y combinator can’t be given a type in the simply typed lambda calculus.

Primitive Recursion

Next, we extend the language with the usual recursor for natural numbers. It’s very similar to the case analysis term, but also provides the successor branch with the recursive result or the “induction hypothesis”.

DefinitionNatural Number Recursor TermGiven terms 𝑡1,𝑡𝑧,𝑡𝑠, the recursor term for natural numbers isdenoted nrec(𝑡1,𝑡𝑧,𝑡𝑠). NotationNatural Number Recursor TermUsually, when working with nrec(𝑡1,𝑡𝑧,𝑡𝑠), 𝑡𝑠 is an abstraction 𝑡𝑠=𝜆𝑝:Nat.𝜆𝑖:𝑇.𝑡𝑝 for some term 𝑡𝑝. We can write the term asnrec(𝑡1,𝑡𝑧,𝑝.𝑖.𝑡𝑝) with the lambdas omitted for clarity. ExampleThe following terms represent addition, multiplication, predecessor andsubtraction functions.plus𝜆𝑛:Nat.𝜆𝑚:Nat.nrec(𝑛,𝑚,𝑝.𝑖.𝑆(𝑖))mult𝜆𝑛:Nat.𝜆𝑚:Nat.nrec(𝑛,𝑍,𝑝.𝑖.plus𝑖𝑚)pred𝜆𝑛:Nat.nrec(𝑛,𝑍,𝑝.𝑖.𝑝)minus𝜆𝑛:Nat.𝜆𝑚:Nat.nrec(𝑚,𝑛,𝑝.𝑖.pred𝑝)

The dynamics of nrec are basically identical to the dynamics presented in the last part in Extensions to Lambda Calculus.

𝑡1CBV𝑡1RecNat1nrec(𝑡1,𝑡𝑧,𝑡𝑠)CBVnrec(𝑡1,𝑡𝑧,𝑡𝑠)RecNatZnrec(𝑍,𝑡𝑧,𝑡𝑠)CBV𝑡𝑧𝑣𝑝valueRecNatSnrec(𝑆(𝑣𝑝),𝑡𝑧,𝑡𝑠)CBV𝑡𝑠𝑣𝑝nrec(𝑣𝑝,𝑡𝑧,𝑡𝑠)

The typing rules for nrec are similar to ncase, but the successor branch takes an additional argument for the recursive result (or induction hypothesis) of type 𝑇. The type of 𝑡𝑠 can be derived by looking at the RecNatS reduction rule.

Let’s say that nrec(𝑆(𝑝),𝑡𝑧,𝑡𝑠) has type 𝑇. RecNatS reduces this to 𝑡𝑠𝑝nrec(𝑝,𝑡𝑧,𝑡𝑠) which to have type 𝑇 requires that 𝑡𝑠::Nat𝑇𝑇. The typing rule is otherwise the same as CaseNat.

Γ𝑡1::NatΓ𝑡𝑧::𝑇Γ𝑡𝑠::Nat𝑇𝑇RecNatΓnrec(𝑡1,𝑡𝑧,𝑡𝑠)::𝑇
Loading Exercise...
Loading Exercise...
Loading Exercise...

Performance Concerns and Optimization for Natural Numbers

Computing with the current style of natural numbers is extremely inefficient — arithmetic with numbers larger than 100 already takes an observable amount of time. This makes the current encoding of natural numbers completely useless from a practical perspective. Ideally we would use big integers to represent natural numbers.

This is the topic of the next chapter.

Footnotes

Footnotes

  1. When a term containing a ncase is well-typed, as simply typed lambda calculus is type safe, 𝑡𝑠 must evaluate to an abstraction resulting in an AppAbs situation.

  2. Some typed languages provide recursion via a primitive operator with type 𝑇,(𝑇𝑇)𝑇 (or via recursive types). In plain STLC, however, the untyped Y combinator itself is not typeable.