Lambda calculus, or λ-calculus for short, is like a language in mathematical logic. It’s all about explaining how computations work, using function abstraction and application, and playing around with variables and substitutions. You can think of it as a one-size-fits-all model for computation—it can mimic any Turing machine.
Lambda calculus 101
For anyone coming from a background of working with imperative programming languages, the experience of programming using a functional programming language can feel surreal. With no recourse to familiar looping constructs like for and while loops, with the glaring absence of assignment statements and familiar data structures, the learning curve consists of not just learning new syntax but also learning a new way of thinking. While the precise syntax and the details depend on the particular functional language being learned, the crucial underlying way of thinking is common across the board for all functional languages. This way of thinking is essentially the lambda calculus.
Lambda calculus was invented by Alonzo Church in the 1930s, who, as part of his inquiry into the foundational questions in mathematics, explored the idea of representing computation using functions. In fact, lambda calculus is equivalent to
This blog discusses some of the essential elements in lambda calculus, and highlights how the symbolic expressions in lambda calculus can be used for computing. In particular, we show how such expressions can be used for implementing data types, as well as for representing arithmetic, logical, and relational operators.
If you’d like to learn more about functional programming, this course is a really good starting point.
Lambda expressions#
The fundamental building blocks of lambda calculus are known as lambda expressions. A lambda expression can be one of the following three things:
-
A variable: A variable is just a name. For example, , , and can serve as variables.
-
A function abstraction: A function abstraction is a function that has no name and takes in a single parameter. The general form of a function abstraction is
Here, the symbol means the start of an anonymous function. The name that follows is the parameter. The dot symbol indicates that the function body is about to begin. The function body consists of a lambda expression.
For convenience, we occasionally use the term lambda function to mean a function abstraction.
-
A function application: A function application consists of any two lambda expressions, and , placed side by side. In a function application such as
, the expression can be thought of as an argument passed to .
And that’s all the machinery at our disposal. Lambda calculus essentially consists of function abstractions and applying them in meaningful ways to other lambda expressions. While that may not sound like much, it’s amazing what can be accomplished using just these definitions.
Examples: Function abstraction#
-
is the identity function. It returns whatever it receives.
-
is an abstraction that takes a lambda expression , ignores it completely, and returns some other lambda expression . For this reason, it can be thought of as the constant function.
-
is an abstraction that takes a lambda expression and applies it to itself.
Convention on spaces: When variable names consist of a single letter, as in the context of this blog, they need not be separated by spaces.
Binding#
-
The variable that appears right after a is said to be bound in the body of that abstraction. All other variables are said to be free.
- In , the variable is bound.
- In , the variable is bound but the variable is free.
- In , the variable is bound, but the variable is free.
Associativity#
- Function application is left-associative. If there are three expressions listed as , then
Scope#
- The body or the scope of a lambda function goes as far to the right as possible. For example, the underbraces show the scope of and in the following lambda expression:
Convention on parenthesization: Do note that the parentheses are part of the syntax to help with the grouping of the function applications. They can’t always be omitted without loss of meaning, but we do make some exceptions to avoid clutter:
- We omit the outermost parentheses around a standalone lambda expression.
- We omit the parenthesis surrounding two variables.
- We omit parentheses that enforce associativity.
Functions on more variables#
Unlike mathematical functions, each lambda function has only one associated variable bound to its scope. To express the idea of a function on more than one variable, the lambda functions can be nested. For example, the function can be seen as a function that takes a parameter and returns a function of that applies to . This is easier to see with parentheses in place:
The following visualization of a lambda expression may help internalize this idea:
Often, a lambda expression such as is written in shorthand by gathering all variables against a single as shown:
How to do computations using lambda expressions#
To do computations with lambda expressions we need to be able to evaluate them, or reduce them to simpler forms. The primary way to express the evaluation of a function is through a rule called -reduction.
Beta reduction
A function application like -reduces to an expression if is obtained by replacing all occurrences of the bound variable in by . This is denoted
In applying -reductions, the leftmost is removed, and the bound variable in the body of the abstraction is substituted by the expression .
This represents the idea of passing as an argument to an actual function call and returning the result.
Let’s look at some examples of function applications that are beta reducible:
- Example 1: The identity function applied to a free variable reduces to that free variable (since the identity function returns its argument.)
-
Example 2: The identity function applied to itself reduces to the identity function.
-
Example 3: A function that applies its argument to itself, when applied to the identity function -reduces to the identity function.
-
Example 4: We can keep applying -reductions to the following expression, but the computation does not terminate.
- Example 5: We illustrate the application of a nested function abstraction through the following set of slides.
Notation: We use the notation to express zero or more beta reductions.
For example,
Note that when a lambda expression can’t be -reduced any further, it’s said to be in normal form. A non-terminating function application that keeps reducing to itself has no normal form. If the normal form of a lambda expression exists, then reducing the leftmost, outermost lambda application is guaranteed to lead to a unique normal form. This reduction strategy is called a normal order reduction.
Alpha conversion#
It doesn’t matter what name we give to a bound variable in a function as long as we do so consistently for all occurrences of the variable in the scope of that abstraction. So is the same abstraction as .
Now, consider the following expression. The variable within the lambda abstraction is a bound variable, but the variable to which the abstraction applies is a free variable. They’re not the same.
After a -reduction is attempted, the variable passed in as the argument becomes a bound variable.
This is called capturing a free variable. Notice how this capturing of inadvertently changed the meaning of the lambda expression to a self-application—which is other than what was intended.
To avoid capturing free variables, a bound variable can be carefully renamed across the entire scope of that abstraction. Such a renaming is called an -conversion.
For the lambda expression from above, if we rename the bound variable as , we get:
This now says what was intended: Apply the free variable to the argument of the lambda function.
Note: We used to explicitly indicate an alpha conversion, but these are usually done implicitly, as needed before applying -reductions.
But where’s the data?
We’re thinking about doing computations. But all we have are functions (abstracted functions, to be precise) and variables. So where’s the data? Well, when all we have is a lambda expression, the lambda expression is the data.
Before going into exactly what that means, we adopt a small notational convenience to keep ourselves from drowning in a frenzy of symbols:
Notation: We give a name to an abstraction. When naming an abstraction, we use the word define before the name given to it.
Let’s begin by defining two boolean values
Representing boolean values#
We name the following two function abstractions as and .
define
define
If we look at the semantics of these abstractions, takes two parameters and returns the first. Whereas takes two parameters and returns the second.
Note: We could have defined these differently, but it’s easier to go with how it’s typically done.
Logical operators
-
Let’s think about how to represent a not operator.
Applying to returns as shown below.
Similarly, applying to returns .
-
Now, let’s think about how to represent an and operator.
If is applied to , the expression reduces to as expected:
Similarly, when applied to any other combination of and , the outcome would be as expected:
-
We can define using a similar idea:
Just as before, the outcome of applying the operator can be verified to behave as expected:
A conditional statement#
The following expression is intended to serve as an if-else expression, where the branching depends on a condition :
define
-
When applied to , the expression is returned:
-
When applied to , the expression is returned:
Church numerals#
We saw above how boolean values can be encoded as function abstractions. The set of non-negative integers can also be encoded similarly. First, a function is defined and is given the name . A successor function applied to is called a . The successor is applied to and is called a , and so on.
-
define
If is a variable representing zero and is a successor function, ignore and return .
-
define
Apply the successor function on .
-
define
First, apply the successor function on to get , and then apply on a second time.
-
define
-
define
Encodings defined this way are called Church numerals.
Terminology note: When referring to a lambda expression representation of a number as a Church numeral, we call it the numeral .
Repetition#
Suppose we want to apply a lambda abstraction repeatedly to another function a total of times, where is known in advance. How would we do this? Repetitions of this kind can be achieved through the application of the following abstraction:
define
We use the variable name to help remember that the function expects the Church numeral .
If is applied to a Church numeral and the expressions and , we see that is applied to repeatedly:
Arithmetic#
How does one add two Church numerals? Let’s first encode something simpler: an increment of one.
-
define
The key idea is that the expression passed in is applied times to and then one more time again to get the Church numeral .
Let’s apply on to get :
-
To represent adding two numbers and , we apply on the Church numeral a total of times:
define
When applied to a Church numeral , the numeral takes as the successor function and as the expression on which is to be applied times. So in fact, such an application would reduce to the following:
Let’s see this in action applied to and :
If you’d like to get practice verifying this in symbolic form by hand, we include the reductions here for assistance:
-
To represent the multiplication of two numbers and , we want to add a total number of times to .
define
Essentially, when applied to a Church numeral , this is equivalent to:
For example, when is applied to and , it is equivalent to:
Note: The names given to the abstraction must be replaced by the abstractions before applying beta reductions.
Predecessor#
Previously, we defined to get the successor of a numeral. It’s not so easy how we would get a predecessor of a numeral . Consider a numeral such as
How do we strip an to get the following?
Intuition: The way around this is to incrementally construct pairs of numbers where increases in steps of one, and then once we have the pair , the second element of the pair is returned to get the predecessor of .
define
The role of the third variable envisioned here is to return one of the two elements in the pair. This can be seen by passing or as an argument to :
- The expression () returns .
- The expression () returns .
Examples of encoded pairs#
Using the definition of , we can encode pairs of consecutive integers.
-
So for example, the pair is codified as the following expression:
define
-
Similarly, we can encode other pairs as lambda expressions:
define
define
Given a pair represented by , we can get the successive pair by using the abstraction (defined earlier for incrementing):
define
Here, the pair applied to selects the first element in the pair . This selected element is used twice in order to construct the next pair. For example, the abstraction applied to gives us the encoding for :
We’d like to apply the NextPair abstraction times starting with the pair .
Note: In starting with , we assume (for convenience) that the predecessor of is .
We apply the abstraction times as follows:
Finally, to select the second element in a pair, we apply that pair to :
define
For example, let’s apply on to see how this works:
We can finally use to return the predecessor of a numeral by selecting the second element of the pair :
define
Relational operators#
Finally, we look at how relational operators such as , , , , (for expressing equality and inequality) are expressed in lambda calculus.
For example, to check if , we can perform the subtraction and see if the outcome is a . Testing for suffices since we defined the predecessor of to be .
We define the abstraction as follows to check for equality of the input with . Later, we’ll use this abstraction to define other operators.
define
Recall that the numeral is defined as , which is exactly the definition of . So when is applied to , it returns as shown below:
When it is applied to a numeral other than , it returns . Let’s see this for the numeral .
Note that the effect of applying to a numeral other than means that is repeatedly applied to . The repeated applications always return .
Greater than or equal to#
To implement , we need to test and see whether it’s . The subtraction is performed by using the predecessor expression on the numeral repeatedly ( times):
define
If is strictly less than , then is not . Otherwise it is .
Similarly, we can construct a lambda expression, say , to implement the less than or equal to operator .
Equality#
For any two numbers and , if and , it implies that . So we can check for equality between two numerals through the lambda expression:
define
Similarly, to check that , we can use the lambda expression:
define
Strict inequality#
Checking for strict inequality can be done using other operators. For example, we can check if is strictly greater than through the following lambda expression:
define
We can check for strictly less than in a similar way.
define
Summary
Let’s recap what we covered:
- We defined lambda expressions to represent boolean values and non-negative integers.
- We saw how to encode the idea of an if-else expression and the idea of repetition.
- We figured out how to build logical operators and arithmetic operators from first principles.
We hope that this was fun for you and that it continues to be useful to you as you learn and grow!
Frequently Asked Questions
What is lambda calculus used for?
What is lambda calculus used for?