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 , and as we are working in natural numbers, the predecessor of is natural to define as .
Applying the predecessor to times we get .
Together these are the following lambda terms:
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>, },}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
we have Ite1/2/3 rules for reducing the subterms inside an if-then-else term
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.
Boolean Reduction Steps
0 / 5 points
Which of the following are single-step Ξ²-reductions?
Ite Reduction Rules
0 / 5 points
Which of the following statements about if-then-else reduction rules are correct?
As we have changed the terms of our language, we need to also make adjustments to various functions:
TrueandFalsedonβt contain any free variables- To compute the set of free variables, we need to also take the union of free variables in subterms of if-then-else:
- Substitution and renaming need to recursively go through if-then-else subterms
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.
Boolean Extension to Lambda Calculus
0 / 60 points
In this assignment, we will extend lambda calculus with booleans. Start by filling in your solutions from the previous exercise.
Required work:
- Update
eq_alpha,free_vars,rename, andsubstto handle the new boolean terms. - Implement the
Itesmall-step rules (ite,ite1,ite2,ite3) and integrate them intostep. - Ensure
multistepandbeta_eqwork with the extended terms. - Implement the boolean operators
and,or,not,nand, andxorusingIte.
Grading
The boolean-aware versions of eq_alpha, substitution, beta-reduction, beta_eq, and the boolean operators are tested.
Terms are compared up to alpha-equivalence.
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:
- Zero
- Successor
- 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>, },}The adjustments to FV and substitution are as follows
-equivalence is adjusted in the straight-forward way as there are no variable binding sites:
Reduction
Extending -reduction for the natural number fragment is also straight-forward. We add a rule for reducing the inner subterm of .
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 scrutineeThe zero case is straight-forward:
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 .
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.
Finally, we want -reduction to reduce all subterms, so we add a rule for every subterm of 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
Now, we can express the addition and multiplication functions as follows
Recursor Evaluation
0 / 5 points
Which of the following recursor evaluations are correct?
Recursor Reduction Rules
0 / 8 points
Which of the following are correct?
Natural Number Extension
0 / 90 points
In this assignment, we will extend lambda calculus with natural numbers (Zero, Succ(t)) and the recursor Rec.
Start by filling in your solutions from the previous exercise.
Required work:
- Update
eq_alpha,free_vars,rename, andsubstto handleZero,Succ, andRec. - Implement the small-step rules for naturals:
succ1,rec,rec1,rec2,rec3, and integrate them intostep. - Ensure
multistepandbeta_eqwork with natural numbers as well. - Implement arithmetic and comparison operators:
plus,mult,minus,le,eq,lt.
Notes:
- The recursor
recshould reducerec(Zero, z, f)tozandrec(Succ p, z, f)tof p rec(p, z, f).
Grading
The nat-aware versions of eq_alpha, substitution, reduction, beta_eq, and the numeric operators are tested.
Now that the set of booleans is disjoint from the set of natural numbers, which was not the case with Church encoded terms ( 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.
Primitive Recursion
0 / 5 points
Which of the following functions are correctly implemented using the recursor?
Extension Design
0 / 5 points
Which of the following statements about language extensions are true?
Conditional Arithmetic Language with Lambda Calculus Runtime
0 / 90 points
In this assignment, we extend the language with AST, desugaring and a parser. Desugaring and parsing are provided for you. You should begin by familiarizing yourself with the starter code. Itβs highly recommended that you download the code to your local machine in order to utilize the provided REPL.
The primary role of this assignment is to give you an idea of what a programming language project structure looks like.
Grading
Your task is to complete the starter implementation by filling in the TODOs in three files:
| File | What to implement |
|---|---|
term.rs | Free-variables, renaming, and capture-avoiding substitution. |
step.rs | Beta-reduction rules, multi-step reduction, and beta-equivalence. |
alpha.rs | Alpha-equivalence for terms. |
You should reuse your solution from the previous assignment. The tests in this assignment are a subset of the tests from the previous assignment, so filling in your previous solution should be enough to pass all of the tests.
NB: Submit only the contents of the src directory in the root of the ZIP-archive. To create a valid archive on the command line, use
cd srczip ../submission.zip *AST
The AST comes with the standard operators from conditional arithmetic. In addition, it can reference names in the surrounding module.
pub enum AST { // ... /// A name in the surrounding module Name(String),}The details of this are not important, but you can study them by reading module.rs.
AST Desugaring
Desugaring the AST to a lambda term is provided for you.
It uses a module as an environment to resolve Names to their corresponding terms.
Parser
A parser is provided with support for the following syntax:
| Construct | Syntax / notes |
|---|---|
| Variables | Identifiers made of letters, digits, underscore, and apostrophe (e.g., x, foo_bar, x') |
| Booleans | true, false |
| Naturals | Non-negative integer literals (e.g., 0, 42) |
| If-then-else | if <cond> then <expr> else <expr> |
| Lambdas | Long form fun x, <body> and short form x => <body> |
| Application | Juxtaposition (left-associative), e.g., f x y means (f x) y |
| Parentheses | (<expr>) to override precedence |
| Arithmetic | *, +, - |
| Comparisons | ==, !=, <, <=, >, >= |
| Boolean ops | not, and, or |
| Module declarations | name = <expr> (one per line, for use in the REPL) |
The parser is also equipped with some basic tests.
REPL
Running the binary starts an interactive REPL:
| Action | How |
|---|---|
| Evaluate an expression | Enter it at the prompt |
| Declare a global name | name = <expr> |
| List globals | :env |
| Show available commands | :help |
| Exit | :quit or :exit (or Ctrl-D) |
| Tracing reduction steps | Prefix with #, e.g., # (w => w w) (w => w w) |
| Interrupt evaluation | Press Ctrl-C |
| Reuse previous result | Use the this global |
If evaluation takes too long, press Ctrl-C to interrupt. The name is still saved.
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:
stepshould attempt theAppAbs,IteandRecrules 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) 4Likewise we could define the ordinal as follows:
omega = Y succNow, try evaluating the following term
0 * omegaIt should not get stuck if the main rule of recursor is applied before Rec1, Rec2 or Rec3.
Try Out in the REPL
0 / 10 points
Whatβs the numerical result of the following program?
ack = m => rec m succ (m' => A_m => n => rec n (A_m 1) (n' => r => A_m r))ack 3 2Try Out in the REPL 2
0 / 10 points
Y = g => (x => g (x x)) (x => g (x x))
omega = Y succ
ack = fuel => rec fuel (m => n => 0) (_p => ack_prev => m => n => if m == 0 then n + 1 else if n == 0 then ack_prev (m - 1) 1 else ack_prev (m - 1) (ack_prev m (n - 1)))Note: you need to interrupt evaluation with Ctrl-C after declaring Y and omega.
Here are the declarations again in a format thatβs easy to copy and paste.
Y = g => (x => g (x x)) (x => g (x x))omega = Y succack = fuel => rec fuel (m => n => 0) (_p => ack_prev => m => n => if m == 0 then n + 1 else if n == 0 then ack_prev (m - 1) 1 else ack_prev (m - 1) (ack_prev m (n - 1)))Compute x = ack 2 2 1, then y = ack 5 2 1 and finally z = ack omega 2 1.
Write the numerical results separated by a space character: x y z
Footnotes
Footnotes
-
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