Theory of Lambda Calculi

Extensions to Lambda Calculus


Learning Objectives

  • You understand how to extend lambda calculus with primitive types and operations.
  • You can extend Ξ²-reduction rules and auxiliary functions for new language constructs.
  • You understand the recursor for natural numbers and how it enables primitive recursion.
  • You can implement boolean and natural number extensions with their reduction semantics.

Introduction

In the previous chapter we looked at how Church encoding could be used as a foundation for boolean and natural number operations. The final language even supported addition and multiplication, but you may have noticed that it had a couple of features missing β€” notably subtraction and comparison. The reason we left these out is that they are quite complicated to encode into plain lambda calculus.

To demonstrate, although we wont be using Church encoding in this chapter, to get subtraction, one would begin by defining the β€œpredecessor” function The predecessor pred returns π‘›βˆ’1, and as we are working in natural numbers, the predecessor of 0 is natural to define as 0. Applying the predecessor to 𝑛 π‘š times we get π‘›βˆ’π‘š. Together these are the following lambda terms:

predβ‰”πœ†π‘›.πœ†π‘“.πœ†π‘₯.𝑛(πœ†π‘Ÿ.πœ†π‘–.𝑖(π‘Ÿπ‘“))(πœ†π‘“.π‘₯)(πœ†π‘’.𝑒)minusβ‰”πœ†π‘›.πœ†π‘š.π‘špred𝑛

Here is the Wikipedia explanation of pred if you are interested.

Using minus and is_zero it would be now easy to check if one number is less than or equal to another.

Instead of continuing this route, we will be adding so called extensions to lambda calculus which which are like primitive types and their operations.

Boolean Extension

We start by extending the notion of a lambda term with the terms True, False and Ite.

#[derive(Debug, Clone)]
pub enum Term {
    // Core lambda calculus
    Var(String),
    Abs { var: String, body: Box<Term> },
    App(Box<Term>, Box<Term>),

    // Boolean extension
    True,
    False,
    Ite {
        cond: Box<Term>,
        if_true: Box<Term>,
        if_false: Box<Term>,
    },
}
NotationTerm (Lambda Calculus with Booleans)We extend the standard lambda calculus notation with the followingfor boolean terms:1.We use true and false to refer to the terms True and False.2.An if-then-else term with condition 𝑑1, then branch 𝑑2 and elsebranch 𝑑3 is written asif𝑑1then𝑑2else𝑑3

This time, instead of desugaring to core untyped lambda caclulus, we extend the 𝛽-reduction so that it works with the boolean extension. As there is nothing to reduce in True and False, they are considered normal forms or values by themselves. However, an if-then-else needs reduction rules.

Just like the App1/2 rules reduce the subterms in applications

𝑑1βŸΆπ‘‘β€²1App1𝑑1 𝑑2βŸΆπ‘‘β€²1 𝑑2𝑑2βŸΆπ‘‘β€²2App2𝑑1 𝑑2βŸΆπ‘‘1 𝑑2

we have Ite1/2/3 rules for reducing the subterms inside an if-then-else term

𝑑1βŸΆπ‘‘β€²1Ite1if𝑑1then𝑑2else𝑑3⟢if𝑑′1then𝑑2else𝑑3𝑑2βŸΆπ‘‘β€²2Ite2if𝑑1then𝑑2else𝑑3⟢if𝑑1then𝑑′2else𝑑3𝑑3βŸΆπ‘‘β€²3Ite3if𝑑1then𝑑2else𝑑3⟢if𝑑1then𝑑2else𝑑′3

Now for the core functionality of an if-then-else: If the condition is true, we reduce the Ite to the β€œif true” branch, and likewise if it’s false.

IteTrueiftruethen𝑑2else𝑑3βŸΆπ‘‘2IteFalseiffalsethen𝑑2else𝑑3βŸΆπ‘‘3
Loading Exercise...

Loading Exercise...

As we have changed the terms of our language, we need to also make adjustments to various functions:

  • True and False don’t contain any free variables FV(true)=βˆ…FV(false)=βˆ…
  • To compute the set of free variables, we need to also take the union of free variables in subterms of if-then-else: FV(if𝑑1then𝑑2else𝑑3)=FV(𝑑1)βˆͺFV(𝑑2)βˆͺFV(𝑑3)
  • Substitution and renaming need to recursively go through if-then-else subterms [π‘₯↦𝑠](if𝑑1then𝑑2else𝑑3)=if[π‘₯↦𝑠]𝑑1then[π‘₯↦𝑠]𝑑2else[π‘₯↦𝑠]𝑑3

Finally, 𝛼-equivalence needs a similar update. Luckily this is not a big deal as the boolean fragment doesn’t include any new variable binding sites.

TrueA⟨π‘₯𝑠,true⟩=π›ΌβŸ¨π‘¦π‘ ,true⟩FalseA⟨π‘₯𝑠,false⟩=π›ΌβŸ¨π‘¦π‘ ,false⟩⟨π‘₯𝑠,𝑑1⟩=π›ΌβŸ¨π‘¦π‘ ,𝑑′1⟩⟨π‘₯𝑠,𝑑2⟩=π›ΌβŸ¨π‘¦π‘ ,𝑑′2⟩⟨π‘₯𝑠,𝑑3⟩=π›ΌβŸ¨π‘¦π‘ ,𝑑′3⟩IteA⟨π‘₯𝑠,if𝑑1then𝑑2else𝑑3⟩=π›ΌβŸ¨π‘¦π‘ ,if𝑑′1then𝑑′2else𝑑′3⟩
Loading Exercise...

Natural Numbers

With booleans it was quite easy to decide which primitive operations to include, namely if-then-else. However, when it comes to natural numbers, we are not going to add numerals, addition, subtraction, etc. but a set of primitives that can be used to define almost every1 function we need:

  1. Zero
  2. Successor
  3. Primitive recursion (i.e. the recursor)

Now is a good time to recap the contents of Objects and Compatibility and specifically what the recursor is. Here is what the new terms Zero, Succ and Rec look like in Rust

#[derive(Debug, Clone)]
pub enum Term {
    // Core lambda calculus
    // ...

    // Boolean extension
    // ...

    // Natural number extension
    Zero,
    Succ(Box<Term>),
    /// Recursor for natural numbers. Takes in the scrutinee, and two branches: the zero case and the successor case.
    /// `rec : forall T, Nat -> T -> (Nat -> T -> T) -> T`
    ///
    /// If the scrutinee is `zero`, reduces to the zero case.
    /// If the scrutinee is `succ(n)`, reduces to the successor case applied to `n` and the "induction hypothesis" `rec n if_zero if_succ`.
    Rec {
        scrutinee: Box<Term>,
        if_zero: Box<Term>,
        if_succ: Box<Term>,
    },
}
NotationTerm (Lambda Calculus with Natural Numbers)1.We use 𝑍 to refer to the natural number Zero.2.We use 𝑆(𝑛) to refer to the successor of the natural number 𝑛.3.We can write a natural number in the natural way, i.e. 0=𝑍,1=𝑆(𝑍),2=𝑆(𝑆(𝑍))…4.For the recursor, we don’t need any new syntax, it appears as aprimitive function constantrec(𝑑𝑠,𝑑𝑧,𝑑𝑓)The parentheses are used to denote the fact that it cannot bepartially applied and always needs three arguments.

The adjustments to FV and substitution are as follows

FV(rec(𝑑𝑠,𝑑𝑧,𝑑𝑓))=FV(𝑑𝑠)βˆͺFV(𝑑𝑧)βˆͺFV(𝑑𝑓)[π‘₯↦𝑠](rec(𝑑𝑠,𝑑𝑧,𝑑𝑓))=rec([π‘₯↦𝑠]𝑑𝑠,[π‘₯↦𝑠]𝑑𝑧,[π‘₯↦𝑠]𝑑𝑓)

𝛼-equivalence is adjusted in the straight-forward way as there are no variable binding sites:

ZeroA⟨π‘₯𝑠,π‘βŸ©=π›ΌβŸ¨π‘¦π‘ ,π‘βŸ©βŸ¨π‘₯𝑠,π‘‘βŸ©=π›ΌβŸ¨π‘¦π‘ ,π‘‘β€²βŸ©SuccA⟨π‘₯𝑠,𝑆(𝑑)⟩=π›ΌβŸ¨π‘¦π‘ ,𝑆(𝑑′)⟩⟨π‘₯𝑠,π‘‘π‘ βŸ©=π›ΌβŸ¨π‘¦π‘ ,π‘‘β€²π‘ βŸ©βŸ¨π‘₯𝑠,π‘‘π‘§βŸ©=π›ΌβŸ¨π‘¦π‘ ,π‘‘β€²π‘§βŸ©βŸ¨π‘₯𝑠,π‘‘π‘“βŸ©=π›ΌβŸ¨π‘¦π‘ ,π‘‘β€²π‘“βŸ©RecA⟨π‘₯𝑠,rec(𝑑𝑠,𝑑𝑧,π‘‘π‘“βŸ©=π›ΌβŸ¨π‘¦π‘ ,rec(𝑑′𝑠,𝑑′𝑧,π‘‘β€²π‘“βŸ©

Reduction

Extending 𝛽-reduction for the natural number fragment is also straight-forward. We add a rule for reducing the inner subterm of 𝑆(βˆ’).

π‘‘βŸΆπ‘‘β€²Succ1𝑆(𝑑)βŸΆπ‘†(𝑑′)

Next, we add rules to reduce a rec-term where the scrutinee is zero or a successor of some term. Let’s start by looking at the type of the recursor in the π•Šπ•‹π•ƒβ„‚++ implementation presented in part 1.

//    any type  scrutinee zero case   successor case     output
//    |         |         |           |                  |
rec : forall T, Nat ->    T        -> (Nat -> T -> T) -> T
//                                      |     |
//                                      |     result from recursive call with p
//                                      predecessor p of the scrutinee

The zero case is straight-forward:

RecZerorec(𝑍,𝑑𝑧,𝑑𝑓)βŸΆπ‘‘π‘§

The successor case is more interesting. When the scrutinee is a successor of some term 𝑝 (for predecessor), we can create the term that calls the recursor for 𝑝, which is rec(𝑝,𝑑𝑧,𝑑𝑓). This term represents the β€œrecursive call” to the function 𝑑𝑓 to get the previous result (also called the induction hypothesis or ih for short).

As 𝑑𝑓 is a function that takes a natural number, we give it the predecessor 𝑝 and the induction hypothesis as arguments. So its 𝛽-reduction rule is the following rule that has no premises.

RecSuccrec(𝑆(𝑝),𝑑𝑧,𝑑𝑓)βŸΆπ‘‘π‘“π‘rec(𝑝,𝑑𝑧,𝑑𝑓)

Finally, we want 𝛽-reduction to reduce all subterms, so we add a rule for every subterm of rec

π‘‘π‘ βŸΆπ‘‘β€²π‘ Rec1rec(𝑑𝑠,𝑑𝑧,𝑑𝑓)⟢rec(𝑑′𝑠,𝑑𝑧,𝑑𝑓)π‘‘π‘§βŸΆπ‘‘β€²π‘§Rec2rec(𝑑𝑠,𝑑𝑧,𝑑𝑓)⟢rec(𝑑𝑠,𝑑′𝑧,𝑑𝑓)π‘‘π‘“βŸΆπ‘‘β€²π‘“Rec3rec(𝑑𝑠,𝑑𝑧,𝑑𝑓)⟢rec(𝑑𝑠,𝑑𝑧,𝑑′𝑓)

Let’s look at how to define the predecessor function first. It’s straight-forward and uses the fact that recursor provides the predecessor of 𝑛 in the successor case

pred=πœ†π‘›.rec(𝑛,𝑍,πœ†π‘.πœ†_ih.𝑝)

Now, we can express the addition and multiplication functions as follows

plus=πœ†π‘›.πœ†π‘š.rec(𝑛,π‘š,πœ†π‘.πœ†ih.𝑆(ih)) mult=πœ†π‘›.πœ†π‘š.rec(𝑛,π‘š,πœ†π‘.πœ†ih.plusπ‘ših)
Loading Exercise...

Loading Exercise...

Loading Exercise...

Now that the set of booleans is disjoint from the set of natural numbers, which was not the case with Church encoded terms (0 is the same as false), we can decide whether to decode the result as a boolean or a natural number based on its shape.

Now that we have built the backend for the language, let’s add a nice frontend with a parser and syntax sugar. We will also refactor the code into modules as we are approaching 1000 lines of code.

Loading Exercise...

Loading Exercise...

Loading Exercise...

General Recursion

We are going to take a peek at the world of recursion with infinities. The examples here should work in the provided REPL. As a note: if the REPL gets stuck, press Ctrl-C to stop.

Note: step should attempt the AppAbs, Ite and Rec rules before every other rule.

The fixed-point or Y combinator in lambda calculus is the following term

πœ†π‘”.(πœ†π‘₯.𝑔(π‘₯π‘₯))(πœ†π‘₯.𝑔(π‘₯π‘₯))

We can use it to implement general recursion, which enables us to write functions that are not primitive recursive1.

The fixed-point combinator doesn’t have a normal form, as it infinitely expands by composing 𝑔 with itself

πœ†π‘”.𝑔(𝑔(𝑔(…)))

Start by typing the following declaration into the REPL. It should get stuck because it tries to normalize the term, so press Ctrl-C right after pressing enter.

Y = g => (x => g (x x)) (x => g (x x))

The Y declaration was saved as a global (which you can list with :env), so we can try to define the factorial function using it:

Y (fact => n => if n == 0 then 1 else fact (n - 1) * n) 4

Likewise we could define the ordinal πœ” as follows:

omega = Y succ

Now, try evaluating the following term

0 * omega

It should not get stuck if the main rule of recursor is applied before Rec1, Rec2 or Rec3.

Loading Exercise...

Loading Exercise...

Footnotes

Footnotes

  1. See Wikipedia: Primitive recursive function. The Ackermann function is a well-known example of a non-primitive recursive function which nevertheless is total and computable. ↩ ↩2