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.
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.
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 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 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.
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.
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. β©