The Untyped Lambda Calculus: Manuel Eberl Eberlm@cs - Tum.edu August 21, 2011
The Untyped Lambda Calculus: Manuel Eberl Eberlm@cs - Tum.edu August 21, 2011
Contents
1 2 Introduction Basics 2.1 Notation . . . . . . . . . . . . . . . . . 2.2 Examples for abstraction and application 2.3 Conversion of expressionsn . . . . . . . 2.3.1 Free and bound variables . . . . 2.3.2 Substitution . . . . . . . . . . . 2.3.3 conversion . . . . . . . . . . 2.3.4 reduction . . . . . . . . . . . 2.3.5 normal form . . . . . . . . . 2.4 non-terminating expressions . . . . . 2.5 Currying . . . . . . . . . . . . . . . . . Encoding data types 3.1 Booleans . . . . . . . . . . . . . . . . 3.2 Pairs . . . . . . . . . . . . . . . . . . . 3.3 Natural numbers - the Church numerals 3.4 Arithmetic operations . . . . . . . . . . 3.4.1 Successor . . . . . . . . . . . . 3.4.2 Addition . . . . . . . . . . . . 3.4.3 Multiplication . . . . . . . . . . 3.4.4 Predecessor and subtraction . . 3.4.5 Comparisons . . . . . . . . . . Recursions Conclusion 2 2 2 3 3 3 4 4 5 5 5 6 7 7 7 8 8 8 9 9 9 10 10 11
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
4 5
Introduction
In the early 20th century, several mathematicians tried to dene basic, abstract systems upon which one can build all of mathematics. In contrast to systems such as Zermelo-Fraenkel, which is based on sets as the most basic structure, some systems based un functions were developed. One of them is the Lambda Calculus. The Lambda Calculus was developed by Alonzo Church in 1928 and its rst version was published in 1932. In the following years, inconsistencies were found in both th eoriginal version of Churchs logic in 1932 and in the revised version. (Kleene-Rosser paradoxon) In 1936, Church reduced his logic to what he called pure Lambda Calculus, the very core of his logic, which is known today as Lambda Calculus. Similarly to older approaches by Peano, Schnnkel and Curry, functions are dened and applied to one another in the Lambda Calculus. The potential of this simple system was underestimated at rst, but it became apparent that the Lambda Calculus is as powerful as the Turing Machine and therefore all computable problems can be expressed in Lambda calculus. The interest in the Lambda Calculus and Currys related Combinatoric Logic was initially very low for several years. In the 1960s, however, interest among computer scientists grew giving rise to functional programming languages such as LISP and Haskell. This paper shall give a short intoduction to the untyped Lambda Calculus.
2
2.1
Basics
Notation
The basic idea of the Lambda Calculus is the following: in one single expression, (anonymous) functions are dened and applied to one another. Each Lambda expression is a function mapping a Lambda expression to a Lambda expression. There are three basic types of expressions which can be nested: Variables, function abstraction und function application. They form the Lambda expressions. Denition 1 (Lambda expression): Variable A Variable x is a Lambda expression. Abstraction Let x be a variable and U be a Lambda expression. The abstraction U is a Lambda expression. Application Let U , V be Lambda expressions. The application U V is a Lambda expression. Additionally, parentheses may be used for grouping. Without parentheses, the following priority rules apply: 1. Abstractions reach as far to the right as they can, i.e. x. a b c is equivalent to x. (a b c), not to ( x. a) b c. 2. The application is left-associative, i.e. U V W is equivalent to (U V ) W , not to U (V W ). Therefore, U V W means: U is applied to V and the resulting function is applied to W , while U (V W ) is a simple chaining of functions and means U is applied to the result of V W . As a notational simplication, nested abstractions can be condensed, e.g. x. y. T may be written as xy. T for better readability. The meaning of the expressions is the following: An abstraction x. U denes an anonymous function x U where U is an expression which may contain 2
x (then the parameter of the function is plugged in for x), but does not have to contain it. (the function is constant in that case) An application applies a function to a parameter.
2.2
In the following section, some simple functions are dened and applied in order to demonstrate abstraction and application and also to illustrate the application of one function to another function. (higher order functions) Examples of simple functions We dened the following functions: id := x. x - expects one parameter and returns it (identity function) twice := f x. f ( f x) - expects a function and another parameter and applies the function to the parameter twice. sqr - expects one parameter and squares it1 These functions can be applied to parameters as shown below. id a: yields a twice id a: yields id (id a), which in turn yields a id id: yields id sqr 3: yields 9 twice sqr 3: yields 81 twice sqr: yields a to the 4th power function Note: there is no way to dene identiers for function - the numbers used above are no symbolic identiers but only shorthands. In reality, a lambda expression is nothing more than a long sequence of variables, abstractions and applications - one has to replace each of this identiiers with the complete, expanded form of the function it represents. Functions cannot be accessed by name. This will be signicant when handling recursion. For an illustration of what a Lambda expression without shorthand looks like, see the example at the and of this paper.
2.3
2.3.1
Conversion of expressionsn
Free and bound variables
Similarly to quantors like and , there are two kinds of variables in Lambda calculus: free and bound. Variables that are dened in a Lambda abstraction are called bound within this abstraction, variables that are not bound are called free. Examples x. x y: x is bound in the abstraction, y is free x. x. x: the x is bound by the inner abstraction and hides the outer x ( x. x) x: the rst x is bound, the second one is free ( x. x) ( y. x): the rst underlined x is bound, the second underlined x is free.
explicitly dened here since encoding numbers in Lambda calculus is explained in a later section. For now, let us simply assume that we can dene numbers and apply arithmetic operations such as sqr to them.
1 Not
A more formal denition of free and bound variable is the following: Denition 2 (Free variables): Let FV (A) be the set of free variables in an expression A. This set is dened recursively: FV(x) = {x} for a variable x FV( x. B) = FV(B) \ {x} for a Lambda expression B and a variable x FV(B C) = FV(B) FV(C) for Lambda expressions B, C
A Lambda expression without free variables is called closed Lambda expression or combinator.
2.3.2
Substitution
An important operation for further conversions is the substitution, i.e. replacing a variable with an expression. Denition 3 (Substitution): The substitution [x := T ] of x with T is dened as: x[x := R] = R a[x := R] = a (for x = a) (S T )[x := R] = S[x := R] T [x := R] ( x. T )[x := R] = x. T (no substitution since x is bound in this case) ( a. T )[x := R] = a. T [x := R] (for x = a and a does not appear as a free variable in R, freshness condition) The last condition (a does not appear as a free variable in R) is very important. Another way to express this is: a must be fresh in R, because substitutions can lead to problems otherwise - when R is plugged in, the a in R are not free anymore but are interpreted as the bound a. (capturing) Example: Let A = a, a narf foo. We want to evaluate A[foo := bar a wuppdi]. To do that, we have to replace all occurences of foo with the expression bar a wuppdi. However, a is not fresh in bar a wuppdi, as the expression contains it as a free variable. A simple substitution would result in the expression a. a narf (bar a wuppdi) in which the a in parentheses is the bound a - not the free one that it should be. This is why the freshness condition is important. The current denition of substitution is only partial. When the freshness condition is not satised, the a in the abstraction must be renamed into a fresh variable with conversion to avoid capturing. With this addition to the denition, substitution is total. 2.3.3 conversion As it is the case with quantors, the names of bound variables are irrelevant. The Lambda expressios x. x and y. y are completely equivalent as they yield the same results for all parameters. The process of renaming a bound variable in an abstraction is called conversion.
Denition 4 ( conversion): The conversion of an abstraction x. S for variables x, y and a Lambda expression S where y is fresh in S is dened as: x. S y. S[x := y] Expressions that can be converted to one another through conversion are called -equivalent. The condition that y is fresh in S ensures that we do not need another conversion to resolve the problem. With that denition, we can solve the above problem of the substituion: ( a. a narf foo)[foo := bar a wuppdi] ( a . a narf foo)[foo := bar a wuppdi] = a . a narf (bar a wuppdi) 2.3.4 reduction reduction is a resolution of an application. For this purpose, we rst dene the term redex (reducible expression):
Denition 5 (Redex): An expression of the form ( x. S) T where x is a variable and S, T are Lambda expressions is called Redex (reducible expression). An expression that does not contain any redexes is called irreducible. The reduction is an operation that transforms a redex as dened above by plugging in the parameter T into S for x. This can be expressed by substitution:
2.3.5 normal form With many Lambda expressions, an irreducible expression can be reached after a nite number of reductions. This irreducible expression is called normal form. An interesting property of the reduction is its conuence, i.e. if an expression A has a normal form, all possible irreducible expressions that can be reached through reduction of A are -equivalent to the normal form; this means that there is at most one reduction, irrespective of the order in which the reductions are performed. However, it is sometimes possible to reduce an expression with a normal form an innite amount of times without ever obtaining a normal form (see next section) if a bad reduction strategy is chosen. But whenever two different reduction processes lead to a normal form, the results are guaranteed to be the same. (save conversion)
2.4
non-terminating expressions
Since the Lambda calculus is Turing-complete, there have to be expressions that do not terminate. This means that they do not have a normal form - no irreducible expression can be obtained through a nite number of reductions. The expression may even grow indenitely as reduction progresses. Because of the Turing-completeness, the question whether a Lambda expression has a normal form is undecidable as it is equivalent to the halting problem. 5
A textbook example of an expression without a normal form is the combinator: := x. x x := = ( x. x x) ( x. x x) The reduction of results in again. Therefore, is invariant under reduction. can also be modied to grow indenitely under reduction: ( x. x x x) ( x. x x x) Reduction leads to: ( x. x x x) ( x. x x x) ( x. x x x) ( x. x x x) ( x. x x x) ( x. x x x) ( x. x x x) ( x. x x x) ( x. x x x) . . .
An example for an expression that has a normal form but also has a reduction strategy that leads to an innite amount of reductions without ever reaching the normal form as stated in the previous section is the following expression: f alse id = ( ab. b) (( x. x x) ( x. x x)) ( x. x) If the redex with the rst underlined abstraction is reduced, the entire expression evaluates to id = x. x in one step. If, however, the redex with the second underlined abstraction is reduced, the expression does not change at all, and this can be repeated innitely. Therefore, while the order of reduction does not change the result, it can indeed determine whether a result is found in the rst place. Recursive functions in particular must be evaluated in a lazy way, i.e. only results that are actually required for the evaluation are calculated, as recursive expression would never terminate otherwise.
2.5
Currying
As stated previously, a Lambda expression is a function that maps a Lambda expression to a Lambda expression. Functions with more than one parameter do not exist in the Lambda calculus - it is, however, possible to build these functions indirectly. If one wants to dene a function such as add that expects two parameters a and b and adds them, one can interpred add as a function of a that returns another function adda , which is effectively an increase by a function, expecting one parameter b and returning a + b. The expression add a b reduces to adda b, which in turn reduces to a + b. add 4, for instance, yields an increase by 4 function. add 4 38 is an application of this increase by 4 function to 38 and the resulting expression reduces to 42. This can be done for every function f with n parameters: convert f to a function mapping the rst parameter a1 to a function fa1 . This function in turn maps the second parameter a2 to fa1 ,a2 and so on, until a function fa1 ,a2 ,...,an1 is reached, which maps the last parameter an to the result fa1 ,a2 ,...,an1 ,an = f (a1 , a2 , . . . , an1 , an ). This is called Currying and it is possible because A A A is isomorphic to A (A (A . . . A)
Denition 7 (Functions with more than one parameter - Currying): Let f be a function of n parameters: f : A1 A2 . . . An B This function can be converted to a higher order function in the following way: f : A1 (A2 (. . . (An B))) Example: Let reverse be a function that takes two functions f and g as parameters and returns g f - i.e. applies g to f . reverse := f g. g f 6
When we apply reverse to two parameters a and b, we get: reverse a b = ( f g. g f ) a b ( g. g a) b = reversea b b a The rst reduction step yields a function reversea which expects a parameter g and applies it to a. The second reduction step then yields the result b a.
The only structures that exist in the Lambda calculus are variables and functions. One might ask how common data types such as booleans and numbers or more complex structures such as lists can be built using only functions. To do this, functions have to be dened with certain behaviour and operations on these data types are realised by applying functions to them. The easiest data type that can be expressed that way are Booleans.
3.1
Booleans
There are two Boolean values - true and false - and four interesting operations on them - the unary not and the ternary and, or, xor. One way to encode true and false is as a function that takes two parameters and returns one of them. true returns the rst one, false returns the second one. The denition of true and false is therefore very simple and intuitive: Denition 8 (true and false): true := ab. a false := ab. b With that, if-then-else branches can be realised: consider the expression a U V where a is a Boolean as dened above and U and V are Lambda expressions. If a = true, V is ignored and the expression reduces to U . If a = false, U is ignored and the expression reduces to V . Therefore, the expression a U V is efectively a branch if a then U else V ;. Using this if branch, all Boolean operators can be dened very intuitively. A not is simply if a then false else true; A and can be expressed similarly: if a and b should be calculated, one can rst look at a. If a = false, the result is false irrespective of b. If a = true, the result only depends on b, as the result is true iff b is true. Therefore, and is nothing more than if a then b else false; We can now dene all four important Boolean operations in the Lambda calculus: operator not and or xor if-then-else if a then false else true; if a then b else false; if a then true else b; if a then not b else b; Lambda expression a. a false true ab. a b false ab. a true b ab. a (not b) b
3.2
Pairs
Encoding pairs (2-tuples) is also very intuitive. What properties is a pair supposed to have? It has to contain two Lambda expressions and they can be accessed. One possible solution to encode this behaviour in function sis the following: A pair is a function that expects another function z as a parameter an applies z to its two values. This means that a pair p = (a, b) maps another function z to z a b.
How can the values a and b be accessed? This is done by two functions called rst and second, which expect a pair p = (a; b) and return a or b, respectively. In order to do that, we have to apply p to a function which returns its rst or second parameter, respectively. We have already dened such functions: true and false. The expression p true evaluates to a and the expression p false evaluates to b. Denition 9 (Pairs): A pair p = (a; b) is encoded as z. z a b. Access to pairs is done with the following to functions: rst := p. p true second := p. p false
3.3
Encoding natural numbers is more difcult. Church used the Church numerals, which were named after him: a natural number n is encoded as a function which maps another function f to f n , i.e. that chains f with itself n times2 , i.e. n : f x f n x, or, as a Lambda expression:
Examples: 0= f x. x (maps every function to id and is interestingly enough equivalent to false) 1= f x. f x 2= f x. f ( f x) ...
3.4
3.4.1
Arithmetic operations
Successor
The most basic operation for working with natural numbers is the successor operation succ : n n + 1. This can be done relatively easily in Lambda calculus: since n is a function that maps f and x to f n x, the successor of n has to be a function that maps f and x to f n+1 x. Therefore, we can calculate f n x, which is n x and apply f to the result once more.
3.4.2
Addition
The addition m + n can be dened in two different ways: one way is to apply the successor function to n m times, i.e. succ n. However, the resulting expressions get rather lengthy when evaluated by hand, which is why we will use another method. We rst calculate n f x - which is f n x - and apply f m to the result, yielding f m+n x. Denition 12 (Addition): + := mn f x. m f (n f x) Example: + 2 3 = ( mn f x. m f (n f x)) 2 3 f x. 2 f (3 f x) = = f x. ( f x . f ( f x )) f (( f x . f ( f ( f x ))) f x) f x. ( x . f ( f x )) (( f x . f ( f ( f x ))) f x) f x. ( x . f ( f x )) ( f ( f ( f x))) f x. f ( f ( f ( f ( f x)))) = 5 3.4.3 Multiplication
In order to calculate multiplication, we consider the function m f . This function expects one parameter x and returns f m x. Simply put, the expanded expression contains a sequence of m f s. So what happens when n is applied to that function? The function is chained n times, i.e. we now have a n sequences of m f s in a row, so this function maps x to f mn x, which is effectively a multiplication. Therefore, we simply have to calculate n (m f ) or analogously m (n f ). Denition 13 (Multiplication): := mn f . m (n f )
3.4.4
Finding a predecessor of a Church numeral in Lambda calculus is signicantly more difcult than nding the successor, as applying a function once more is simple - but applying it once less is not. Since we have natural numbers, 0 does not have a predecessor. For simplicity, however, we will dene pred 0 = 0, . 1 or in other words: pred n := n . . n = max(m n, 0). Therefore, if m < n, m . n = 0, since we are is a saturated subtraction. i.e. m . trying to avoid negative numbers. If m n, m n = m n. The Peano axioms demand that the 0 not have a predecessor, but in this context it is more useful to articially dene it as its own predecessor. To facilitate the calculation of the predecessor of n, we dene a helper function , which does the following mapping of a pair p = (a, b) to another pair: : (a, b) (a + 1, a) := pz. z (succ (rst p)) (rst p)
When this function is applied to (0, 0), it returns (1, 0). If it is in turn applied to (1, 0), it returns (2, 1) and so on. Following this pattern, the result of n chained applications of the function to (0, 0) is the pair . 1). Therefore, the predecessor we are looking for is the second element of this pair. (n, n Thus, we get: pred := n. second (n z. z 0 0) . n as By analogy to the rst variant presented for addition, we can dene the (saturated) subtraction m a n applications of the predecessor function to m: . := mn. n pred m 3.4.5 Comparisons
The most basic comparison is the test whether a number is equal to 0. This will enable the construction of more advanced comparisons later. In order to compare a Church numeral to 0, we use the fact that 0 maps every function f to the identity function, i.e. ( f )[0 f id] We can now construct a helper function h, which takes one parameter x and always returns false. When n is applied to h, we can distinguish two cases: If n = 0, then n h = id and n h returns its parameter. If n = 0, then n h ignores its parameter and returns false. Therefore, if h is chained n times and applied to true, we obtain an iszero function. Denition 14 (Test for 0 - iszero): iszero := n. n ( x. false) true In order to dene our actual comparison operators, we consider the operator and rewrite its denition: . n = 04 m n mn 0 m . . Conversely, we can obtain a operator by swapping the Thus, we can reduce to iszero and parameters. Negating the and operators yields > and < and combination of and with a Boolean and, we can obtain a =. Denition 15 (Comparison operators): . m n) := mn. iszero ( . := mn. iszero ( n m) > := mn. not ( m n) < := mn. not ( m n) = := mn. and (( m n) ( m n))
Recursions
We have already implicitly introduced a recursion with xed recursion depth - each application of a Church numeral n to a function f applies f n times recursively to itself. (see e.g. pred). However, this is vaguely similar to a for loop or primitively recursive approach. (cf. LOOP computability) Recursive functions with a variable termination condition would be more interesting. As an example, we will use the following recursive denition of the factorial n!:5 n! : =
4 see
1 if n = 0 n (n 1)! otherwise
denition of the saturated subtraction factorial, of course, is also primitively recursive and LOOP-computable, it could be dened in the same way as the predecessor function, with a helper function such as the that was used in pred
5 The
10
Supercially, one could construct the following Lambda expression: fac_rec := n. (iszero n) 1 ( n (fac_rec (pred n))) This, however, leads to a problem: fac_rec is used in its own denition. But at this point, it is not fully dened yet. If one were to try and expand the shorthands in the expresion above, one would have to replace fac_rec with the entire expression again, yielding an expression that again contains fac_rec, which would have to be expanded again and so on. The expanded expression would have innite length. The problem, in a nutshell, is, that fac_rec does not know itself and thus cannot call itself. A simple way to resolve this is to add another parameter to fac_rec - namely a function that is assumed to be fac_rec. Instead of directly calling fac_rec in its denition, it calls this function f and passes f to it as an additional parameter. We can now dene fac_rec: fac_rec := f n. (iszero n) 1 ( n ( f f (pred n))) To obtain a more convenient function, we can now dene an encapsulating function fac, which hides this workaround: fac := n. fac_rec fac_rec n
Finally, in order to illustrate what a complete, expanded Lambda expression looks like, we shall give an expanded version of fac 3 without any shorthands in order to appreciate the complexity that results from these several layers of abstraction that we have dened: ( n. ( f n1 . (( n2 . n2 ( x. ( ab.b)) ( ab.a)) n1 ) ( f x. f x) (( mn2 f . m (n2 f )) n1 ( f f (( n3 . (n3 ( pz. z (( n4 f x. f (n4 f x))( p ( ab.a))) ( p ( ab.a))) ( z. z ( ab.b) ( ab.b))) ( ab.b)) n1 )))) ( f n1 . (( n2 . n2 ( x. ( ab.b)) ( ab.a)) n1 ) ( f x. f x) (( mn2 f . m (n2 f )) n1 ( f f (( n3 . (n3 ( pz. z (( n4 f x. f (n4 f x))( p ( ab.a))) ( p ( ab.a))) ( z. z ( ab.b) ( ab.b))) ( ab.b)) n1 )))) n) f x. f ( f ( f x)) This expression evaluates to: f x. f ( f ( f ( f ( f ( f x))))) = 6
Conclusion
The untyped Lambda calculus is, just like the Turing machine, a minimal, abstract model for computability and therefore a useful tool for proofs in theoretical computer science. However, it is completely unsuitable for actual programming. The biggest problem is the lack of typing - a function for Booleans can be applied to Church numerals or even arithmetic operations. When this happens by accident, the mistakes can be difcult to trace. In general, calculations in typed Lambda calculus are unnecessarily convoluted, intricate and impractical. Just like the Turing machine it is merely an abstract model; applying it for actual computations is not very sensible. This problems can be solved by introducing native types for Booleans, numbers, lists and so on instead of using the rather pedestrian encodings. If some other modications are applied an syntactic sugar is added to make the calculus more usable, the result is a complete functional programming language such as LISP or Haskell. Because of their tremendously useful properties, Lambda expressions and derived functional concepts have also been introduced to imperative and object-oriented languages such as Python, Scala and - indirectly - even C++.
11