Dakotah Lambert

collected musings

Foundations: The λ-Calculus

2024 Nov 12 at 01:05

Many are familiar with the concept of the Turing machine, the mathematical model of computation that gives us the phrase “Turing Complete”. A Turing machine is built of several parts: a finite alphabet of symbols, an infinite tape divided into cells marked by these symbols, a read/write head that can modify the tape, and a finite set of control states that determine what should be written and how the tape should be moved depending on what is read. Less familiar but much simpler is the λ-calculus of Alonzo Church. Equivalent in expressive power to the Turing machine, the λ-calculus has only the creation and application of single-argument functions.

  • An identifier x is an expression representing a parameter
  • If E is an expression and x an identifier, then ( λx. E ) is an expression representing a function.
  • If E and F are expressions, then (EF) is an expression representing the application of E to F.
  • Nothing else is an expression.

To improve readability by avoiding excessive parentheses, function application is associated to the left: wxyz ( ( (wx) y ) z ) . Further, parentheses are omitted from functions that are created but not applied: λf. λx. λy. fyx ( λf. ( λx. ( λy. ( fy ) x ) ) ) , distinct from λf. λx. ( λy. fy ) 𝑥 ( λf. ( λx. ( λy. ( fy ) ) x ) ) .

Programs are built from data and transformations. In a high-level language, the job of a programmer is to define the structure of relevant objects and some sequence of valid transformations to convert input states into desired output states. This structure applies to all computational formalisms. For a Turing machine, objects must be encoded as some linear sequence of symbols on the tape. For the λ-calculus, objects must be encoded as some sort of single-argument function. Here, we’ll explore representations of some basic objects.

Propositional Logic

Arguably the simplest data structure is the humble Boolean, with two distinct values: true and false. How might one represent a Boolean value as a single-argument function? To answer this, consider how Boolean values are used. Typically, they are used in conditional expressions: if some condition C holds, then X, else Y. Instead of defining true and false as atomic values, we can define them in terms of this conditional structure. If C is a Boolean value, we can have CXY produce X when C is true, and produce Y when C is false.

This looks like we are considering two-argument functions, but we need not take more than one at a time. Let us define the Boolean value “true” as 𝐓. We want 𝐓XY to be X. In other words, we want (𝐓X) to be a function of one argument which returns X no matter its input: 𝐓 λx. λy. x . In action: 𝐓XY ( λx. λy. x ) XY ( λy. X ) Y X . (This process of selecting an argument and instantiating it inside the function body is called β-reduction.) Similarly, 𝐅 selects the second of the two arguments: 𝐅 λx. λy. y . In action: 𝐅XY ( λx. λy. y ) XY ( λy. y ) Y Y .

Boolean combinators such as AND, OR and NOT, are naturally two-argument functions. First consider the OR combinator, which we will denote . Recall that Booleans are essentially two-argument functions that represent if-then-else constructions. It should be that PQ is true if P is true, else it is true if Q is true and false otherwise. In fully expanded form: PQ P 𝐓 ( Q𝐓𝐅 ) . Notice, however, that ( Q𝐓𝐅 ) is equivalent to Q: if Q𝐓 then 𝐓 is selected, and if Q𝐅 then 𝐅 is selected, so assuming Q is a Boolean value, we can simplify this definition to PQ P𝐓Q . Notice also that, if P is a Boolean value and selects the 𝐓 here, then P must have been true; in other words, this simplifies further to PQ PPQ .

Doing β-reduction in reverse, we can “unapply” the function one argument at a time. This is called β-abstraction: If PQ PPQ , then PQ ( λq. PPq ) Q . In other words, P λq. PPq . After another round of β-abstraction, given P λq. PPq we derive P ( λp. λq. ppq ) P . In other words, λp. λq. ppq . Whenever we see a wrapped application λx. Ex , where this x does not appear in E, we can apply a rule called η-reduction to transform this into just E. So λp. pp .

Similarly, AND (denoted ) can be defined as λp. λq. pqp . If the first argument is false, it should select something falsey, which it itself is. So it can select itself! But if it is true, then the outcome depends on the second operand: the outcome is true if and only if that second operand is true, so the second operand itself is the right thing to return.

Finally, NOT (denoted ¬) returns false if the operand is true and returns true if the operand is false: ¬ λp. p𝐅𝐓 .

Just like with circuits, more complex operations can be built by composing these simple pieces. For instance, the exclusive-or operation (XOR, denoted ) is defined such that λp. λq. ( pq ) ( ¬ ( pq ) ) ; the first operand is true or the second is true, but not both. Alternatively: λp. λq. p ( ¬q ) q .

OperationExpression
𝐓 λx. λy. x
𝐅 λx. λy. y
¬ λp. p𝐅𝐓
λp. pp
λp. λq. pqp
λp. λq. p ( ¬q ) q

Natural Numbers

Just as Boolean values were represented as functions based on their use in if-then-else constructions, so too are natural numbers represented based on their use. What is, say, three? In the λ-calculus, threeness is doing something thrice. A natural number n is a function which takes as its argument a function f and returns f composed with itself for a total of n applications. So 𝟑 λf. λx. f ( f ( fx ) ) .

Those familiar with Peano numbers know that it suffices to define a zero and a successor function. The result of applying a function zero times to an argument x is just x itself, so 𝟎 λf. λx. x . This should look familiar: 𝟎 takes two arguments and returns the second. In other words, 𝟎𝐅. The successor function takes a number and adds one to it, applying the function one more time. 𝐒 λn. λf. λx. f ( nfx ) .

We want 𝟏 to be the identity function such that 𝟏fx fx . Also, 𝟏 𝐒𝟎 . Check: 𝐒𝟎 ( λn. λf. λx. f ( nfx ) )𝟎 λf. λx. f ( 𝟎fx ) λf. λx. fx λf. f , the identity function, as desired.

The successor function 𝐒 adds one to its first argument. To add m is to apply the successor function m times. But repeated application is exactly what these numbers represent! The sum of m and n is simply m𝐒n. Define addition in exactly that way: + λm. λn. m𝐒n . By η-reduction, + λm. m𝐒 . And multiplication of m by n is simply to add m copies of n: 5×3 3 +3 +3 +3 +3 . That is, × λm. λn. n ( n𝐒 ) 𝟎 . As a simpler alternative, × λm. λn. λf. n ( n𝐒 ) f .

Tying in to the previous section, sometimes one wants to query whether a number satisfies some property. One simple property is whether the number is zero. In order to do this, we need a function that takes an argument and returns false: λx. 𝐅 . If this function is applied one or more times to an argument, the result is false. But if it is applied zero times to an argument x, then the result is x itself. Define 𝐙 λn. n ( λx. 𝐅 ) 𝐓 . Then 𝐙𝟎 𝟎 ( λx. 𝐅 ) 𝐓 𝐓 , but 𝐙𝟐 𝟐 ( λx. 𝐅 ) 𝐓 ( λf. λx. f(fx) ) ( λx. 𝐅 ) 𝐓 ( λx. ( λx. 𝐅 ) ( ( λx. 𝐅 ) x ) ) 𝐓 ( λx. 𝐅 ) ( ( λx. 𝐅 ) 𝐓 ) 𝐅 . Recall how the Boolean values are defined; λx. 𝐅 is simply 𝐓𝐅. So, 𝐙 λn. n (𝐓𝐅) 𝐓 .

To determine whether two arbitrary numbers are equal is more difficult. There is no built-in comparison function, no equality operator that we can leverage. Instead of directly defining equality, it is easier to first define inequality, , and to derive equality from that. We begin by defining a clamped predecessor function 𝐏. If x is zero then 𝐏x is also zero, otherwise it is x1. In order to do this, we introduce a representation of pairs. Then we can define a function f(a,b) ( a+1 , a ) such that f(0,0) (1,0) , f( f(0,0) ) f(1,0) (2,1) , and so on. Then the result of n applications of f to (0,0) will contain n1 in its second component.

The pair (a,b) can be represented by the function λf. fab . Applying a select-first function yields a and applying a select-second function yields b. We have those, they are 𝐓 and 𝐅, respectively. Our partial-predecessor function is then λp. λf. f ( 𝐒 (p𝐓) ) (p𝐓) . Take a pair p (a,b) and return a new pair whose first component is a+1 and whose second component is just a. Then 𝐏 λn. n ( λp. λf. f (𝐒 (p𝐓) ) (p𝐓) ) ( λf. f𝟎𝟎 ) 𝐅 .

With that in place, we have clamped subtraction, sometimes called “monus”. The difference xy is y iterations of the predecessor function to x: λx. λy. y𝐏x . As 𝐏𝟎 𝟎 , we have that xy is zero if and only if xy. Define λx. λy. 𝐙 ( xy ) . Two natural numbers are equal if each is less than or equal to the other; define equality, =, as λx. λy. ( xy ) ( yx ) . The other possibilities can easily be defined in terms of these.

ObjectExpression
𝟎 λx. λy. y
𝐒 (successor) λn. λf. λx. f ( nfx )
𝐏 (clamped predecessor) λn. n ( λp. λf. f (𝐒 (p𝐓) ) (p𝐓) ) ( λf. f𝟎𝟎 ) 𝐅
𝐙 (zero?) λn. n (𝐓𝐅) 𝐓
+ λm. m𝐒
(“monus”) λm. λn. n𝐏m
× λm. λn. λf. m(nf)
λm. λn. 𝐙 ( mn )
= λx. λy. ( xy ) ( yx )

Integers

So far, we have the natural numbers, the nonnegative numbers. We also have pairs and Boolean values, so one way to represent all integers would be to encode a pair containing a sign bit and a magnitude. Those familiar with foundational set theory, however, may be aware of a different representation, which does not use Boolean values. Instead, integers are represented by pairs, where (x,y) represents the integer xy. Each possible integer has infinitely many representative pairs, but this representation is easier to work with.

For instance, addition is pointwise. The pair (x,y) represents xy, and (a,b) represents ab, and ( x+a , y+b ) represents (x+a) (y+b) x +a y b (x𝑦) + (ab) . Recall that a pair selects a function and applies it with its two components as arguments, in order, and that 𝐓 selects the first and 𝐅 selects the second. Define + Z λp. λr. λf. f ( + (p𝐓) (r𝐓) ) ( + (p𝐅) (r𝐅) ) ; take two pairs p and r representing integers, then return the pair obtained by adding each component separately.

If (x,y) represents xy, then (xy) yx is represented by (y,x). Negation is to flip the pair: 𝐍 λp. λf. f (p𝐅) (p𝐓) . To subtract two integers, we can define Z λm. λn. +Z m (𝐍n) . After expansion and simplification, Z λp. λr. λf. f ( + (p𝐓) (r𝐅) ) ( + (p𝐅) (r𝐓) ) .

Recall that ( xy ) represents max( xy , 0 ) . If xy, this is 0, but if xy, this is xy. By trichotomy, + ( xy ) ( yx ) is the absolute value of x𝑦. Thus, to obtain the absolute value of an integer as a natural number, we define 𝐀 λx. + ( x ) ( x ( λa. λb. a𝐏b ) ) λx. + ( x ) ( x ( λa. a𝐏 ) ) . To test whether an integer is zero is to test whether its absolute value is zero, so 𝐙 Z λn. 𝐙 (𝐀n) .

Finally, to multiply integers, note that the distributive property holds: k (xy) kx ky . So we want to multiply each component of one pair by the absolute value of the other pair, and negate the result if necessary. Define × Z λm. λn. m 𝐍 ( 𝟎𝐍 ) ( λf. f ( × (𝐀m) (n𝐓) ) ( × (𝐀m) (n𝐅) ) ) .

OperationExpression
𝐀 (absolute value) λn. + ( n ) ( n ( λa. a𝐏 ) )
𝐙Z λn. 𝐙 (𝐀n)
+Z λm. λn. λf. f ( + (m𝐓) (n𝐓) ) ( + (m𝐅) (n𝐅) )
Z λm. λn. λf. f ( + (m𝐓) (n𝐅) ) ( + (m𝐅) (n𝐓) )
×Z λm. λn. m 𝐍 ( 𝟎𝐍 ) ( λf. f ( × (𝐀m) (n𝐓) ) ( × (𝐀m) (n𝐅) ) )

Lists

With numbers available, one might naturally want to represent a list of numbers. Recall that in defining integers we used pairs, represented by functions that take (essentially) two-argument functions as input. Given a pair p of natural numbers, p𝐓 is the first, p𝐅 is the second, p+ is their sum, and so on. To represent a list, we might consider a similar sort of configuration: a list L is a function which takes as its arguments a binary operation f and a default value x. If the list is empty, then application yields the default value, Lfx x . Otherwise, the list has a head h and a tail t, and the application yields Lfx fh (tfx) .

This gives us exactly the information that we need in order to construct something akin to a linked list. Like zero and falsity, the empty list ignores its first argument and returns its second: 𝟎 . Then to cons a head h onto a tail t, we can use 𝐂 λh. λt. λf. λx. fh (tfx) .

A function which takes a list of integers and returns their sum is Σ λa. a +Z ( λf. f𝟎𝟎 ) . The product is Π λa. a ×Z ( λf. f ( 𝐒𝟎 ) 𝟎 ) . To get the length, the function should ignore the head and return the successor of the result, and the default value should be the natural number zero: 𝐋 λa. a ( λh. 𝐒 ) 𝟎 . To extract the head, or return 𝐅 in the case of an empty list, define 𝐇 λa. a𝐓𝐅 . Finally, to extract the tail, we use a technique similar to that used for finding the predecessor of a natural number. The initial value is a pair consisting of two copies of the empty set. The binary operation takes the head h of the list and such a pair (x,y), and returns ( 𝐂hx,x ) . The tail in the end is the second component: λa. a ( λh. λp. λf. f ( 𝐂h (p𝐓) ) (p𝐓) ) ( λf. f ) 𝐅 . This, with repeated application gives an indexing function: drop n elements and take the head: @ λa. λn. 𝐇( n𝑎 ) .

OperationExpression
(empty) 𝟎
𝐂 (cons) λh. λt. λf. λx. fh (tfx)
Σ (sum) λa. a +Z ( λf. f𝟎𝟎 )
Π (product) λa. a ×Z ( λf. f ( 𝐒𝟎 ) 𝟎 )
𝐋 (length) λa. a ( λh. 𝐒 ) 𝟎
𝐇 (head) λa. a𝐓𝐅
(tail) λa. a ( λh. λp. λf. f ( 𝐂h (p𝐓) ) (p𝐓) ) ( λf. f ) 𝐅
@ (index) λa. λn. 𝐇( n𝑎 )

Recursion

To this point, none of our functions have been recursive. This is both because we have not needed such power and because the λ-calculus does not support recursion at all. Suppose we want to find the sum of the natural numbers up to and including n. Now, of course, we know that the result should be n(n+1) / 2 , but, alas, we have yet to define a division function. Rather than incorporate division, let us consider how we might compute this result.

One method is to create a function φ that will map a pair (x,a) to a new pair ( x+1 , a+x+1 ) . Applying this function thrice to (0,0) proceeds as follows: (0,0) becomes (1,1), which becomes (2,3), which finally becomes (3,6). In general, upon n applications, the first component is n and the second component is the sum of the natural numbers up to and including n. Because numbers in the λ-calculus represent repeated application, this satisfies the requirement. Define φ λx. λf. f ( 𝐒(x𝐓) ) ( + ( 𝐒(x𝐓) ) (x𝐅) ) . Then define Δ λn. nφ ( λf. f𝟎𝟎 ) 𝐅 to compute the desired sum. This is essentially a functionalization of the iterative solution to the problem.

Another high-level approach would be to use a recursive function: Δ(n) is 0 if n is 0, else it is n + Δ( n1 ) . But we cannot say [invalid: Δ λn. 𝐙n𝟎 ( + n ( Δ(𝐏n) ) ) ], because this kind of self-reference creates an infinitely long formula. What we can do is supply as an argument the function to be used as the “recursive” call: Δ λf. λn. 𝐙 n 𝟎 ( + n ( f(𝐏n) ) ) . Then, as long as we provide a sufficiently large stack of Δ, we can recursively compute the solution: Δ( Δ( Δ( ΔΔ )) )𝟑 ( + 𝟑 (Δ (Δ (Δ Δ) )𝟐) ) ( + 𝟑 ( + 𝟐 (Δ( ΔΔ) 𝟏))) ( + 𝟑( + 𝟐( + 𝟏( ΔΔ𝟎 )))) ( + 𝟑( + 𝟐( + 𝟏( 𝟎)))) 𝟔 . Then what we want is some way to get a sufficiently tall stack of Δ, no matter the parameter.

Enter the 𝐘 combinator, defined such that 𝐘f f(𝐘f) . We cannot define it this way, because this uses recursion. Instead, we define 𝐘 such that it accepts a function as an argument, then reconstructs itself as an argument to that function: 𝐘 λf. ( λx. f(xx) ) ( λx. f(xx) ) . Then applying this to a function g yields 𝐘g ( λf. ( λx. f(xx) ) ( λx. f(xx) ) ) g ( λx. g(xx) ) ( λx. g(xx) ) g ( ( λx. g(xx) ) ( λx. g(xx) ) ) 𝑔(𝐘g) . Now, we have 𝐘 Δ 𝟑 Δ(𝐘Δ) 𝟑 ( + 𝟑( 𝐘Δ 𝟐)) ( + 𝟑( Δ( 𝐘Δ) 𝟐)) ( + 𝟑( + 𝟐( 𝐘Δ 𝟏))) ( + 𝟑( + 𝟐( Δ(𝐘Δ) 𝟏))) ( + 𝟑( + 𝟐( + 𝟏( 𝐘Δ𝟎 )))) ( + 𝟑( + 𝟐( + 𝟏(Δ( 𝐘Δ) 𝟎)))) ( + 𝟑( + 𝟐( + 𝟏( 𝟎)))) 𝟔 , as desired. We need not concern ourselves with providing an appropriate number of Δ, because 𝐘Δ always produces another.

Directions

This post sampled a small collection of common objects and data structures to serve as an introduction to the λ-calculus and the functional approach to computation. Unlike a Turing machine or a personal computer, there is no notion of a memory layout, no restriction to a linear pattern of cells. Even so, structuring data requires careful thought. Using the provided structures and applications as a guide, consider trying these additional challenges. For how many of them is 𝐘 necessary?

  • Define the missing inequalities on natural numbers.
  • Define equality and inequalities on integers.
  • Define the factorial function on natural numbers, where the factorial of zero is one, and the factorial of any n greater than zero is the product of n and the factorial of n1.
  • Define the integer-division of natural numbers or integers.
  • Construct the rational numbers along with appropriate addition, subtraction, multiplication and division.
  • Implement natural number exponents on natural numbers or integers.
  • Implement integer exponents on rational numbers.
  • Implement some representation of a binary tree.