Lambda Calculus: Introduction

Lambda calculus, λ-calculus, is a formal system in mathematical logic for expressing computation, and in that aspect it is equivalent to a Turing machine.

Lambda Calculus is a prototype programming languages invented by Alonzo Church in the 1930's, a functional language based on function abstraction and function application, using parameter binding and substitution.

Lambda Calculus is a higher-order language: it provides a systematic syntax for functions whose input and output values are other functions.

Lambda Calculus offers alternative approach to the representation of natural numbers. Majority of the set theories define, e.g. number 3, as the property that all sets containing 3 elements have in common; other set theories (e.g. stratified) define number 3 as the set that contains all sets with 3 elements. Set theories are built around the concept of a set as the most primitive notion, but, in Lambda Calculus a function plays that role.

In fact, only functions exist in Lambda Calculus. Everything else (boolean constants, numbers, data structures, etc.) has to be defined from scratch. This process begins by defining a function abstraction (i.e. a function definition) then, somewhat arbitrarily, assigning a meaning to it.

Lambda Calculus defines natural numbers in terms of functions (of course, since there's nothing else), that is, number 3 is represented as applying a function 3 times to its argument. Akin to other definitions of natural numbers, number zero is stated as an axiom and the successor function, that produces the subsequent natural numbers, is defined. So a number n is an n-fold of successor function to the zero argument.

Lambda calculus consists of constructing lambda terms and performing reduction operations on them.

Informal introduction

Lamda calculus has only 3 kinds of expressions: variables, abstraction (i.e. function definition) and function application.

  • Variables:

    infinite set of vars (with sub/superscript if needed) e.g. a,f,x2a,f,x_2\dots

  • Abstraction:

    is (anonymous) function definition, e.g. λx.x\lambda x.x defines a function that takes a parameter, xx, and returns the evaluated body (the expression after the dot), in this case just xx (id function).

  • Application: applying a function to an expression, e.g. (λx.x)(y)(\lambda x.x)(y) applies the id function to yy.

fxfx, associate to the left: fxyzfxyz is ((fx)y)z((fx)y)z

associate to the right: λf.xyz\lambda f.xyz λf.(xyz)\lambda f.(xyz)

Formal introduction

  • Assume given an infinite set V\mathcal{V} of variables, denoted by x,y,zx, y, z\dots

  • The set of lambda terms is given by the following Backus-Naur Form

    (MM and NN are lambda terms, xx is a variable)

    M,N::=x  (MN)  (λx.M)M, N ::= x \ |\ (MN)\ |\ (\lambda{x}.M)

Traditional introduction:

  • Assume given an infinite set V\mathcal{V} of variables

  • Let AA be an alphabet consisting of the elements of V\mathcal{V} and the special symbols: λ, ., (, )

  • Let AA^∗ be the set of strings (finite sequences) over the alphabet AA

  • The set of lambda terms is the smallest subset ΛA\Lambda \subseteq A^∗ such that:

    • Whenever xVx\in \mathcal{V} then xΛx\in \Lambda

    • Whenever M,NΛM,N\in \Lambda then (MN)Λ(MN)\in \Lambda

    • Whenever xVx\in \mathcal{V} and MΛM\in \Lambda then λx.MΛ\lambda{x}.M \in \Lambda

Syntax

There are 3 kinds of λ-expressions: 1. Variables, v0,v1,v2,v_0,v_1,v_2,\dots, e.g. x,y,z,x,y,z,\dots 2. Application, e1e2e_1e_2, which means e1(e2)e_1(e_2) i.e. application of expression e1e1 to expression e2e2. 3. Abstractions, λx.e\lambda x.e, represent the anonymous function which evaluates to (returns) the value ee when given formal parameter xx.

Association

  • Application, abcdabcd, associate to the left: ((ab)c)d((ab)c)d

  • Abstraction, λf.λg.λh.x\lambda f.\lambda g.\lambda h.x, associate to the right: λf.(λg.(λh.x))\lambda f.(\lambda g.(\lambda h.x))

Convention

  • Juxtaposed abstraction, λa.λb.λc.abc\lambda a.\lambda b.\lambda c.abc, can be shorthanded as λabc.abc\lambda abc.abc

  • Parenthesis are used to disambiguate expressions or to enforce evaluation order: application abcdabcd is interpreted as ((ab)c)d((ab)c)d, to enforce right-associativity, write a(b(cd))a(b(cd))

Abstraction

A lambda function is an abstraction over a lambda expression:  λx.e\quad \ \lambda x.e where xx is a formal parameter and ee is a (sub)expression.

Examples of abstractions:

 λx.x λfs.f λaf.fa\quad \ \lambda x.x\\ \quad \ \lambda fs.f\\ \quad \ \lambda af.fa
  • Greek letter lambda introduces a name used for abstraction.

  • taht name is function's bound variable (here xx) and it represents a formal parameter similar to function declarations in PL.

  • The dot separates the name (formal parameter) from the function's body.

  • function's body is an expression in which abstraction with that name (actual parameter) takes place.

  • expression (in function's body) may be any lambda expression, including another function abstraction.

Similarity with programming languages

Lambda abstraction (assuming the function + was defined, as infix):  λx.λy.x+y\quad \ \lambda x.\lambda y.x+y

// in js
let f = x => y => x + y;
// in rust
let f = move |x| move |y| x + y;
(* in ocaml *)
let f x y = x + y;;

</details>

Variables

Variables that are named in an abstraction are considered bound; e.g. λx.xyx is a lambda expression in which the variable x occurs bound, while the variable y occurs free.

If a bound parameter does not appear in the body of a function, it just gets thrown away.

(λx.yz)ayz(\lambda x.yz)a\\ yz
  • An occurrence of a variable x within a subexpression e in λx.e is a bound variable.

  • An occurrence of an unbound variable is a free variable.

  • The occurrence of x in the abstraction's head, λx., is the binding occurrence, which introduces the variable x

  • The occurrence of bound x in the abstraction's body is called applied occurrences.

  • If some occurrence of x is free in the subexpression then that is a free occurrence of variable x.

  • A variable x is bound by the syntactically innermost enclosing λ, if one exists, just as in any block-structured programming language.

  • Closed expression contains no free variables.

  • Open expression contains free variables.

  • Combinators are lambda abstractions without free variables.

The same variable may occur both bound and free in an expression. For example, the first applied occurrence of xx in (λx.λy.yx)((λz.zx)x)(\lambda x.\lambda y.yx)((\lambda z.zx)x) is bound, but the second and third applied occurrences are free.

We can index all occurances of x  (λxb.λy.yx1)((λz.zx2)x3)\quad \ (\lambda x_b.\lambda y.yx_1)((\lambda z.zx_2)x_3) to better see that the first occurance of xx i.e. x1x_1 is bound by xbx_b, while the two other occurances, x2x_2 and x3x_3, are free.

Alpha-conversion

The substitution of ff for the free occurrences of xx in ee, written e[f/x]e[f/x], is defined:

  • x[f/x]:=fx[f/x] :=f

    and for a variable yy, if y≢xy\not \equiv x, y[f/x]:=yy[f/x]:=y

  • For applications, we substitute into the two parts:

     (e1 e2)[t/x]:=(e1[t/x] e2[t/x])\quad \ (e_1\ e_2)[t/x] := (e_1[t/x]\ e_2[t/x])

  • If eλx.ge\equiv \lambda x.g then e[f/x]:=ee[f/x] := e

    If yy is a variable distinct from xx, and eλy.ge\equiv \lambda y.g then

    • if yy does not appear free in ff,

      e[f/x]:=λy.g[f/x]e[f/x]:=\lambda y.g[f/x]

    • if yy does appear free in ff,

      e[f/x]:=λz.(g[z/y][f/x])e[f/x] := \lambda z.(g[z/y][f/x])

      where zz is a variable which does not appear in ff or gg.

      Note that we have an infinite collection of variables,

      so that we can always find such a "z".

  • In general, it is easy to see that if xx is not free in ee

    then e[f/x]e[f/x] is ee

Application

A function application specialises an abstraction by providing a value for the name (formal parameter). The function expression contains the abstraction to be specialised with the argument expression. In a function application, also known as a bound pair, the function expression is said to be applied to the argument expression.

When the function (λx.xy)(\lambda x.xy) is applied to argument 33, (λx.xy)3(\lambda x.xy)3:

  • the value of the argument (33) is assigned to the formal parameter xx

  • the head of the function is dropped (λx.\lambda x.)

  • the actual parameters in the function's body (bound params with the same name as the formal param) are replaced with the value 33

  • the expression evaluates to 3y3y

The process of evaluation of a λ-expression is called simplification or β-reduction (beta-reduction).

In general, to evaluate application of a lambda abstraction, i.e.  (λx.e1)e2\quad \ (\lambda x .e1)e2 we replace the actual parameters in the function's body with the formal parameter - this substitution is denoted by e1[e2/x]e1[e2/x].

Lazy and strict evaluation

There are two approaches to evaluating a function applications:

  • strict evaluation: by value i.e. applicative order

  • lazy evaluation: by reference i.e. normal order

The former approach is called applicative order and it is similar to "call by value" in PL in that the value of the argument is evaluated before being assigned as the value of the formal parameter.

The second approach is called normal order and it is similar to "call by reference" in PL in that the value of the argument is assigned to the formal parameter unevaluated.

All occurences of the function's bound variable in the function's body expression are replaced by either the value of the argument expression or the unevaluated argument expression.

In most cases the evaluation order amounts to the same result, but sometimes the order does matter, to the point of evaluation decidibility.

For example

(λx.x)(((λxy.x)a)b)(\lambda x.x)(((\lambda xy.x)a)b) here the argument expression, (((λxy.x)a)b)(((\lambda xy.x)a)b), is either evaluated first and only then applied to the abstraction, (λx.x)(\lambda x.x), or it is applied immediately unevaluated:

by value (evaluate the arg before applyng it):

 (λx.x) (((λxy.x)a)b) (λx.x) ((λy.a)b) (λx.x) (a) a\quad \ (\lambda x.x)\ (((\lambda xy.x)a)b)\\ \quad \ (\lambda x.x)\ ((\lambda y.a)b)\\ \quad \ (\lambda x.x)\ (a)\\ \quad \ a

by reference (apply the arg unevaluated):

 (λx.x) (((λxy.x)a)b) (((λxy.x)a)b) ((λy.a)b) a\quad \ (\lambda x.x)\ (((\lambda xy.x)a)b)\\ \quad \ (((\lambda xy.x)a)b)\\ \quad \ ((\lambda y.a)b)\\ \quad \ a

</details>

Currying

Lambda functions take one parameter at the time. In order to supply more then one parameter, there can be one function per argument: the first function accepts the first parameter and returns the second function that accepts the second parameter (with the first parameter fixed), and so on.

The function λxy.xy\lambda xy.xy is the same as λx.λy.xy\lambda x.\lambda y.xy, which is the same as λx.(λy.xy)\lambda x.(\lambda y.xy).

When it is applied, (( (λx.(λy.xy)) a) b)((\ (\lambda x.(\lambda y.xy))\ a)\ b), the argument aa is bound by the outer abstraction (parameter xx), while the argument bb is bound by the inner (parameter yy):

( (λx.(λy.xy)) a) b(λy.ay) bab(\ (\lambda x.(\lambda y.xy))\ a)\ b\\ (\lambda y.ay)\ b\\ ab

Examples

Examplary expressions

Example: Evaluating a lambda expression (assuming the function + was defined, as infix)

((λx.λy.x+y)(5))(2)(λy.5+y)(2)5+27((\lambda x . \lambda y .x+y)(5))(2)\\ (\lambda y.5+y)(2)\\ 5 + 2\\7

Example: Evaluation i.e. beta reduction

( ( λf.ff )  (λx.x) )3( (λx.x)(λx.x) )3( λx.x )33(\ (\ \lambda f.ff\ )\ \ (\lambda x.x)\ )3\\ (\ (\lambda x.x)(\lambda x.x)\ )3\\ (\ \lambda x.x\ )3\\3

Example: evaluation fork

( (λxy.xy) (λf.f) ) a( (λx.(λy.xy)) (λf.f) ) a(λy.(λf.f)y) a(λy.y) a(λf.f)aaa(\ (\lambda xy.xy)\ (\lambda f.f)\ )\ a\\ (\ (\lambda x.(\lambda y.xy))\ (\lambda f.f)\ )\ a\\ (\lambda y.(\lambda f.f)y)\ a \quad\\ (\lambda y.y)\ a \quad | \quad (\lambda f.f)a\\ a \quad \quad \quad \quad | \quad a

Example: constant function (unused bound parameter)

(λx.λy.z) (λf.f)λy.z(\lambda x.\lambda y.z)\ (\lambda f.f)\\ \lambda y.z

Example expressions:

(λx.y)zλx.yzλf.fxy=λf.((fx)y)(\lambda x.y)z \neq \lambda x.yz \\\quad\\ \lambda f.fxy = \lambda f.((fx)y)

</details>

Last updated