Natural Numbers and Arithmetic
Learning Objectives
- You understand Church encoding of booleans and natural numbers in lambda calculus.
- You can implement boolean operators and arithmetic operations using Church encodings.
- You can encode and decode data types between lambda calculus and host languages.
- You understand how to build sugared syntax on top of lambda calculus.
Introduction
In this chapter, we will start exploring the surprising fact that lambda calculus has a lot of data types βhidden insideβ. As a trivial example, abstractions in lambda calculus are exactly functions from functional programming. However, most would not consider functions to be data types, so what actual data types are definable in lambda calculus?
As it turns out, most of them: booleans, natural numbers, pairs, sum types, lists and algebraic data types in general. So lambda calculus is a surprisingly general and capable programming language.
We begin by learning about the Church encoding of the booleans, and then define natural numbers in lambda calculus. By the end of this chapter, we have a basically developed the language of conditional arithmetic on top of lambda calculus. You get to implement some of details like encoding and decoding Rust data types to and from lambda calculus.
In the next part, we will learn about recursion, which lambda calculus is also capable of!
As an excellent source for learning more, check out chapter 3 of Selinger: Lecture Notes on the Lambda Calculus.
Thinking With "Types" in Lambda Calculus
0 / 5 points
Which of the following statements are true for the code to make sense from a βtype perspectiveβ?
You can interpret S, T and U to be any types.
Church Encoded Booleans
Consider the following terms which are syntactically similar, but semantically quite different
The first two terms () have the same shape, but are clearly different as the variable inside the inner abstraction refers to the first or the second argument respectively. The third is the identity function, which is distinct from the other three, although applying to anything gets us . The fourth term is not closed, unlike the other three. Itβs hard to say how should behave as its behavior depends on .
Next, consider the following terms non--equivalent terms
Notice that as -reduces to the identity function , we consider the terms -equivalent. -equivalence is the characterization of having βsame behaviorβ1 with the condition that the terms normalize. We wonβt study diverging terms as their (multi-step) -reduction isnβt a total function.
Next, we try to analyze the behavior of , as they have a special meaning when viewed from a certain perspective.
We begin by observing the the terms and can be thought of as two-argument functions that return one of the two arguments. Observed from a type-theoretic perspective, the functions have the type for any type . As it turns out, these two are the only two inhabitants of this type signature, up to -equivalence.
This stems from the fact that the functions must be parametric in the generic type , which means that they must not use any information about what can do. If was a function type, we could apply it to some term, but we cannot assume that is a function in general.
Another thing to note, the two functions must be closed to have a type, i.e. does not have type , or any type at all, if we know nothing about the type of .
Hopefully you are somewhat convinced that these two terms are the only -distinct inhabitants of the boolean-like type , so we can name them as
Next, letβs think about what boolean operators would look like.
We want to find a term that acts like the logical conjunction, a term and which satisfies the following βtruth tableβ
As
trueandfalseare normal forms, butand t1 t2is not, the -equivalence can be equivalently replaced with .
Based on the type of and, it should take two booleans and return a boolean, so we can start defining it with nested abstractions:
Next, we need to fill in the hole using a term which evaluates to true if both and behave like true.
Letβs refine and further by writing inside it.
Now the only thing to do is to return , if both and are true, and otherwise. Think about how to accomplish this task for a moment and answer the following quiz.
Church Encoding of Logical And
0 / 5 points
Which of the following are correct regarding filling the hole in the logical and operation and = Ξ» bβ. Ξ» bβ. Ξ» x. Ξ» y. ???
First, letβs try to use .
If is false, we would like to return as and false _ is always false.
This is enough information to refine the function further.
If, on the other hand, is true, we need to check and return if is also true, and otherwise. Therefore, the final hole can be filled with
Letβs check the βtypesβ to make sure we didnβt make a mistake.
Consider that have type , for any , and that hav type .
The result of applying to and results in a value of type , and applying to and which are both of type results again in , so the type of and is
We use the polymorphic type quantifier to indicate that can be applied to two term of any type to get .
The type of booleans in Church encoding is , and we have checked that and indeed takes two (Church) booleans and returns a (Church) boolean.
Notice that the type is alpha-equivalent with as and are bound type variables, and consistently renaming to (or vice versa) gets us one from the other.
As the lambda calculus we are working with is untyped, the types here are for demonstration purposes only β implementing polymorphic type checking is outside the scope of the course.
Church Booleans
0 / 60 points
First, fill in your previous implementations.
Implement Church booleans:
- Start with encoding and decoding:
encode_boolanddecode_bool.encode_bool(true)must return the termx => y => x. decode_boolmust only accept one of the Church encoded boolean terms, otherwise it should returnNone.- Next, implement basic boolean operators:
not,nand,or. - Finally, implement
if_then_elseandxor.
Use the provided t! macro to build terms. To embed an existing Term inside t!, wrap it with !(...) and clone it when reused (e.g., t!(!(op) !(t.clone()))).
You can also embed a boolean operator you have already defined using t!(!(and()) ...) for example.
Grading
The functions above are tested. For boolean operators, the results are normalized before comparison.
Church Encoded Natural Numbers
The next set of lambda terms we will study is the Church encoding of natural numbers also called Church numerals. The idea is simple, a Church numeral is an abstraction over two variables (like the boolean) but instead of returning either on directly, it applies the first iteratively to the second.
As there can be any positive number of (or zero) occurrences of the first argument, we have an infinite set of numerals.
To get an idea of how Church numerals behave, letβs use the following abstraction as :
Here plus_one is simply a free variable with an unusual name.
Next, consider what happens when we apply different numerals to this abstraction:
For larger numerals we get
If instead, we used the negation of Church booleans, i.e. , we would get
Which is again equal to the input not up to alpha equivalence.
However, applying to not (composing not with itself twice) results in
Applying this to any Church boolean returns back the same boolean, so itβs like a complicated way to express the identity function on booleans.
Church Numeral Evaluation
0 / 5 points
Which of the following statements about Church numeral evaluation are correct?
Church Encoding Theory
0 / 8 points
Which of the following theoretical statements about Church encodings are true?
Church Numeral Arithmetic
As you have learned, itβs possible to implement various boolean operators directly in lambda calculus. Next, we will look at some examples of arithmetic operators that work with Church numerals.
First, to compare a Church numeral with zero, we can use for primitive βcase matchingβ.
If , it returns its second argument, which should be true.
And if is not zero, we need to return false so we use a constant function in place of ?
For example evaluates to
Other arithmetic operators are defined as follows
Have a look at the Wikipedia article: Church encoding for more concrete examples.
0 = false
Due to the decision to define false as rather than the other way around, we notice that the set of Church encoded booleans and numerals intersect:
As an exercise, think about if or not.
Unfortunately this intersection of the sets means that there isnβt a general decoding function from Church encoded terms as it would have to decide whether to decode to an integer or a boolean.
Arithmetic Operations with Church Encoding
0 / 5 points
Which of the following statements are correct?
Each number refers to its Church encoded counterpart.
Likewise true and false are the Church encoded versions of the booleans.
Church Numerals
0 / 60 points
First, fill in your previous implementations.
Implement Church numerals:
encode_natanddecode_nat- arithmetic operators
plusandmult
Suggested approach:
- For
encode_nat(n), buildf => x => f (f (... x))withnapplications. - For
decode_nat, accept only terms of the shapef => x => f (f (... x))and returnNonefor anything else. Notice that the variable names need not befandx. - Use the
t!macro and embed existing terms with!(...), cloning when reused.
Grading
The functions above are tested. For arithmetic, results are normalized before comparison.
Church Encoding as Sugar
As we now know how to define basic functions in Church numeral arithmetic, letβs design a programming language with sugared surface syntax which uses lambda calculus under the hood.
Syntax Sugar
0 / 5 points
Consider a language with syntax sugar for natural numbers and arithmetic which can be desugared to lambda calculus using Church encoding. Which of the following statements are correct?
Desugaring From an AST to Lambda Terms
0 / 5 points
Which statements about desugaring from abstract syntax to lambda calculus are correct?
Desugar to Church
0 / 60 points
First, fill in your previous implementations.
Next, implement the desugar function which transforms an AST to an equivalent lambda Term.
Use the t! macro and embed terms with !(...).
For example, desugaring an application from the abstract syntax is a straight-forward recursive call:
App(t1, t2) => t!(!(t1.desugar()) !(t2.desugar()))Grading
Only the desugar method is tested.
The terms it returns are tested up to beta-equivalence, i.e. they need to behave correctly (reduce to the correct value).
Footnotes
Footnotes
-
There is also an even more general version of βsame behaviorβ which combines -equivalence with so called -equivalence. -equivalence expands equality between lambda terms to be extensional. For example the terms and are -equivalent even though they are not -equivalent. β©