Courtesy of CS 145
Lambda calculus is a minimal system that models computation using functions built up from variables, function definition, and function application. Despite its simplicity, lambda calculus is Turing complete, meaning it can model any computation
In essence, the lambda calculus is the most simple form of programming
Some key things in lambda calculus include:
Some key things that lambda calculus does NOT include:
Today, I want to go over how to build these features. I'll show how to encode booleans as a gentle introduction. Then we will jump into the crux of how we do recursion in the lambda calculus
I'll be using scheme/Racket to write all my lambdas
; True
(lambda (x y) x)
; False
(lambda (x y) y)
True and False are used to distinguish between two different things, and from here we can make all logical operators
In essence, that's what True and False are — simply a way to distinguish one thing from another
NOT
; NOT
(lambda (bool)
(bool
(lambda (x y) y) ; FALSE
(lambda (x y) x) ; TRUE
)
)
If bool
is TRUE, it will return (lambda (x y) y) — which is
false
If bool
is FALSE, it will return (lambda (x y) x) — which
is true
AND
; AND
(lambda (bool1 bool2)
(bool1
bool2 (lambda (x y) y)
)
)
AND will only return true if both bool1
and
bool2
are both true. You can step through this example by
setting up a truth table and evaluating all possible combinations of
booleans
This is called β-reduction and is extremely important for writing complex programs in the lambda calculus
OR
; OR
(lambda (bool1 bool2)
(bool1
(lambda (x y) x) bool2
)
)
XOR
; XOR
(lambda (bool1 bool2)
(
bool1
(NOT bool2)
bool2
)
)
For XOR, I am cheating a bit here by using a defined version of NOT. This goes against the rules of lambda calculus where we do not store any functions in memory. Here I use the keyword NOT as a shortcut so I don't need to write the entire lambda within the function
Don't think about how we derived each of these expressions (that was Alonzo Church's job)
Just know that we can represent any data expression in the lambda calculus — as a challenge, think about how you'd implement an If statement in the lambda calculus
There are many ways to define natural numbers in lambda calculus. I won't go over them in this blog, but I'll link some great videos.
The primary modes of expression natural numbers
Great, now that we know the basics of how the lambda calculus work, let's see how we do recursion in it
"Defining things in terms of themselves"
Popular example — factorials
(define (factorial n)
(if (= n 0)
1
(* n (factorial (- n 1)))
)
)
Well, it all has to do with self-application!
The simplest type of recursion is the following
(define (loop)
(loop)
)
To evaluate loop, we call loop, which makes use evaluate loop, and so on
This loop is not legal in the lambda calculus, since we are defining the keyword loop and having to save it in memory
How can we just do this with anonymous lambdas?
(
(lambda (x) (x x))
(lambda (x) (x x))
)
=>
(
(lambda (x) (x x))
(lambda (x) (x x))
)
We call this the Z-Combinator, an infinitely expanding lambda expression. It's a bridge to helping us define general recursion in the lambda calculus, and repeated computation
$$ \lambda f. (\lambda x. (f (x,x))) (\lambda x. (f (x,x))) $$ The Y Combinator is a way to name your function in a language that does not allow such pointers. It proves that function names, classes, etc. are just syntactic sugar
Think of it as a beautiful quine that gives our meaningless lambdas life — the bridge to Turing completeness
Take this factorial example
#lang lazy
(define fact
(lambda (n)
((
(lambda (self)
(lambda (x)
(if (= x 0)
1
(* x ((self self) (sub1 x))))))
(lambda (self)
(lambda (x)
(if (= x 0)
1
(* x ((self self) (sub1 x)))))))
n)))
We use define just to set fact as a temporary place holder, but otherwise we aren't storing any memory pointers for funcitons
The trick here is passing a duplicate of the function into itself again, so it can be ran just like our Z Combinator
(lambda (self)
(lambda (x)
(if (= x 0)
1
(* x ((self self) (sub1 x))))))
this accepts the argument self
to be, well, itself, in the
second part where it's copied. It then also takes in x to be n
through the outermost layer
You can also see this in the recursive step where the function breaks itself down
((self self) (sub1 x))
This will repeat and repeat and create more and mroe functions until our
call stack is complete at the end when x=0
and our entire
function can resolve
ALSO — extremely important point
We are using LAZY evaluation here. That means that we do not try to evaluate the entire function immediately, but only each step as it came
If we tried resolving a whole stack of calls to make pre-compile, we
would run out of memory, as our function would be a repeat of
f(f(f(Y(f))))
so on and so on
Here's a more pure implementation of the Y combinator in Racket
#lang lazy
(define Y
(lambda (f)
((lambda (self) (f (self self)))
(lambda (self) (f (self self))))))
(define factorial
(Y
(lambda (fact)
(lambda (n)
(if (= n 0)
1
(* n (fact (- n 1))))))))
(displayln "Factorial of 5 is: ")
(displayln (factorial 5))