Quicksort written in Python only using lambdas!
GitHub: https://github.com/lucasoshiro/lambdasort
Lambda calculus
Python, just like many other programming languages, supports high-order
functions and anonymous functions. This means, basically, that we can
do this:
# anonymous function (lambda). This is equivaltent to sqrt:
lambda n: n ** 0.5
# function that takes another function as parameter
def apply_func(func, parameter):
return func(parameter)
# function that returns another function. in this case, this is a mutiplier factory
def multiplier_factory(n):
return lambda x: n * x
Right. This way, functions are values that can be created and returned by
other functions.
What if we write a code that the type of every value is a function? For example:
t = lambda a: lambda b: a
f = lambda a: lambda b: b
x = (lambda a: lambda b: a(b)(f))(t)(f)
At first look it seems to be useless. However, believe it or not (spoiler: this
will be explained further), that is a boolean and
of True
and False
!
This is the famous lambda calculus. It only has functions that get
functions as parameters and return other functions. For us, that is enough, but
if you want to read more about it, I really suggest you
this article on Wikipedia.
Even though it seems to be insufficient to do anything, actually lambda calculus
can solve any problem that can be solved with an algorithm, as it is
Turing-complete! Alan Turing
itself has proven it!
The excellent Programming with Nothing
by Tom Stuart shows how to write a fizzbuzz using lambda calculus in Ruby.
When I read it the first time I wanted to do something similar to it, so I made
this: A QUICKSORT IN LAMBDA CALCULUS.
I will tell you all the steps of how I did that!
The beginning: a normal quicksort in Python
The first step was to write a quicksort in Python, but with a difference
compared to the traditional quicksort: it doesn’t change the original list,
instead, it returns a new list, just like the sorted
in Python:
def car(A):
return A[0]
def cdr(A):
return A[1:]
def cons(A, x):
return [x] + A
def concat(A, B):
return A + B
def quicksort(A):
if len(A) <= 1: return A
L, R = partition(A)
p = car(R)
L = quicksort(L)
R = quicksort(cdr(R))
return concat(L, concat([p], R))
def partition(A):
p = car(A)
L, R = [], []
for x in cdr(A):
if x < p:
L, R = cons(x, L), R
else:
L, R = L, cons(x, R)
L, R = L, cons(p, R)
return L, R
Take a look in the use of the functions car
, cdr
and cons
. I followed the
Lisp nomenclature for those functions. They will be further
implemented the same way as some Lisp dialects, so I tried to be closer to them
in how lists work instead of the usual in Python:
- The
car
function returns the first element - The
cdr
function returns a list with the rest of the elements. In other
words, all the elements except the first - By now, I also implemented
cons
as a function that returns a new list that
looks the same as the provided but appending a new element to its
beginning, butcons
is more than that (I’ll discuss it later) - The
concat
function concatenates two lists.
The rest is only a standard quicksort:
-
The
partition
function splits a list in two, the left one being a list
that all of its elements are lesser than the first element of the right one
(the pivot), and the right one being a list that all elements are greater or
equal to the pivot. -
The
quicksort
function callspartition
to split the list that we want to
sort into two and sorts each one usingquicksort
itself, recursively,
being the base of the recursions lists with length lesser or equal to 1.
As for the weird constructions L, R = ...
, by now it is useless to do those
parallel attributions, but this will be helpful in the future.
You can see it here.
Redefining types
As the idea is to rewrite quicksort using only lambdas, we need to somehow represent
data using only functions. The types here are:
- integers (the values in the list that we want to sort)
- lists
- pairs (used by the functions that return more than one value)
- booleans (used by the verifications)
Luckily, the creator of lambda calculus, Alonzo Church has also shown us how
to do that. There’s an article about it
on Wikipedia.
Booleans
Let’s start by the easier ones, booleans:
#boolean constants
LAMBDA_TRUE = lambda a: lambda b: a
LAMBDA_FALSE = lambda a: lambda b: b
#boolean opearations
LAMBDA_OR = lambda a: lambda b: a(LAMBDA_TRUE)(b)
LAMBDA_AND = lambda a: lambda b: a(b)(LAMBDA_FALSE)
LAMBDA_NOT = lambda a: a(LAMBDA_FALSE)(LAMBDA_TRUE)
Yeah, true
is only a function that takes two arguments and returns the
first one, and false
is a function that takes two arguments and returns
the second one.
I know that you’re thinking: “it should be lambda a, b: a
and lambda a, b:
, why there are two
blambdas
?”. This is because in the definition of lambda
calculus, functions can only take one argument. Contrary to that definition,
in Python lambda
can take zero or more arguments, but here I’m restricting
myself to only use functions that take only one argument in order to stick to
the definition. This way, what would be written as lambda a1, a2, a3, ..., an:
will be written as lambda a1: lambda a2: lambda a3: ... lambda an:
. When
calling the function, we use (a1)(a2)(a3)...(an)
instead of (a1, a2,
. The name of that conversion is
a3, ..., an)
currying, named after Haskell
Curry. An example of language that natively uses currying to handle function is
Haskell (you don’t say!).
After that, I implemented the tree basic boolean operations:
-
not
: takes a Church boolean, and calls it using as parametersfalse
and
true
. If the boolean istrue
, it returns the first argument (false
);
if it isfalse
, then it returns the second argument (true
). -
or
: takes two Church booleans, calls the first one passing as arguments
true
and the second boolean. If the first argument ofor
istrue
, then
it returns its first argument (true
); if it isfalse
, then it returns
its second argument (the second argument ofor
). -
and
: much likeor
. Try to simulate it mentally 😉
We can also define an if
:
if
LAMBDA_IF = lambda c: lambda t: lambda e: c(t)(e)
In if
, c
is the condition; t
is the “then” block, what happens when the
condition is true, and e
is the “else” block, what happens when the
condition is false.
This way, if
is closer to an if-expression in Python (or ternary operator)
than a control flow if
:
use_5 = LAMBDA_TRUE
dont_use_5 = LAMBDA_FALSE
a = LAMBDA_IF(use_5)(5)(0) # a = 5
b = LAMBDA_IF(dont_use_5)(5)(0) # a = 0
Conversion
I also defined two functions to convert Church booleans to Python booleans
(l2b
) and vice-versa (b2l
):
#boolean conversion
def l2b(l):
return l(True)(False)
def b2l(b):
return LAMBDA_TRUE if b else LAMBDA_FALSE
Mental exercise: simulate them!
You can see it here
Integers
Church numerals are defined as:
LAMBDA_ZERO = lambda p: lambda x: x
LAMBDA_ONE = lambda p: lambda x: p(x)
LAMBDA_TWO = lambda p: lambda x: p(p(x))
# etc
This is, all the integers are functions that take two arguments p
and x
this way:
- 0 is a function that returns
x
(just likefalse
) - 1 is a function that returns
p(x)
- 2 is a function that returns
p(p(x))
and so on. However, we can represent negative numbers using this encoding.
Increment and decrement
Increment is easy to define, it is a function that adds another layer of p(...)
:
LAMBDA_INCREMENT = lambda l: lambda p: lambda x: p(l(p)(x))
But decrement is far harder. Explaining it takes some time, and
understanding it is not useful for us right now. If you really want to know how
it works, try it by yourself. If you don’t care, just trust me Church that
it works:
LAMBDA_DECREMENT = lambda n: lambda f: lambda x: n(lambda g: lambda h: h(g(f)))(lambda y: x)(lambda y: y)
If you tried to simulate it by yourself, you probably tried to figure out what
happens if you decrement zero. And you found that the decrement of zero is
zero here. Sadly this is a limitation, and we’ll need to remember it soon in
the future.
Add and subtraction
Ok, we have increment and decrement. If we increment n
times a number m
,
then we’ll have m + n
, and if we decrement n
times we’ll have m - n
.
We can define add and subtraction this way:
LAMBDA_ADD = lambda m: lambda n: n(LAMBDA_INCREMENT)(m)
LAMBDA_SUB = lambda m: lambda n: n(LAMBDA_DECREMENT)(m)
Note that increment and decrement will passed as the p
argument of the number
n
and m
will be the x
argument. In other words, m
will be incremented or
decremented the same amount of times than the number of calls that p
has
in n
, this is, n
times. Very, very beautiful.
Even so, a consequence of the decrement of zero being zero here is that n - m
will always be zero if m > n
.
Multiplication and division are also possible, but they won’t be useful for this
quicksort.
Comparisons
The integer operations that will actually be useful for implementing quicksort
are the comparisons. Let’s start by defining the function that tells if a
Church number is or not zero (mental exercise: why does this work?):
LAMBDA_EQZ = lambda n: n(lambda x: LAMBDA_FALSE)(LAMBDA_TRUE)
Using only the operation of equals zero, combined with boolean and
arithmetic operations we can define the others:
m <= n
:(m - n) == 0
(remember:m - n = 0
ifn > m
)m == n
:(m <= n) and (n <= m)
n < m
:(m <= n) and not (m == n)
Or, in Python / lambda calculus:
LAMBDA_LEQ = lambda m: lambda n: LAMBDA_EQZ(LAMBDA_SUB(m)(n))
LAMBDA_EQ = lambda m: lambda n: LAMBDA_AND(LAMBDA_LEQ(m)(n))(LAMBDA_LEQ(n)(m))
LAMBDA_LESS = lambda m: lambda n: LAMBDA_AND(LAMBDA_LEQ(m)(n))(LAMBDA_NOT(LAMBDA_EQ(m)(n)))
Conversion
If we want to convert a Church number to a Python integer we’ll only need to
pass the increment function as the first argument (p
) and 0 as the second:
def l2i(l):
return l(lambda x: x + 1)(0)
We can do the inverse: we can increment several times the Church zero until
we reach the number:
def i2l(i):
l = LAMBDA_ZERO
for j in range(0, i):
l = LAMBDA_INCREMENT(l)
return l
We can also define functions to convert Python integer lists into Church number
lists and vice-versa:
def llist2pylist(L):
return list(map(l2i, L))
def pylist2llist(L):
return list(map(i2l, L))
Using Church numbers
As we can convert Python integer lists into Church number lists and vice-versa
and we can compare Church numbers, we can apply the first change to Quicksort:
def quicksort(A):
if len(A) <= 1: return A
L, R = partition_wrapper(A)
p = car(R)
L = quicksort(L)
R = quicksort(cdr(R))
return concat(L, concat([p], R))
def partition_wrapper(A):
B = pylist2llist(A)
L, R = partition(B)
return llist2pylist(L), llist2pylist(R)
def partition(A):
p = car(A)
L, R = [], []
for x in cdr(A):
if l2b(LAMBDA_LESS(x)(p)):
L, R = cons(x, L), R
else:
L, R = L, cons(x, R)
L, R = L, cons(p, R)
return L, R
The partition
function now operates over Church number lists. In order to
do that, we replaced x < p
by LAMBDA_LESS(x)(p)
, that returns a Church
boolean instead of True
and False
. I needed to use l2b
to convert the
Church boolean to a Python boolean so we can keep the compatibility with if
.
The function partition_wrapper
adapts the new partition
in a way that it
takes Python integers, but partitioning using the new partition
function.
In the following sections I will make several substitutions of types, functions
and operators by functions in lambda calculus, just like I did so far. I’ll try
to change only what is relevant for each step, using the conversion functions if
necessary.
You can see it here.
Pairs and lists
Our basic data structure is the pair. The pair is really a pair of values,
just like a Python tuple of size 2. In Church encoding, a pair and its basic
operations are defined like this:
LAMBDA_CONS = lambda a: lambda b: lambda l: l(a)(b)
LAMBDA_CAR = lambda p: p(lambda a: lambda b: a)
LAMBDA_CDR = lambda p: p(lambda a: lambda b: b)
The first function, LAMBDA_CONS
, defines the pair. Note that, when passing two
values as its arguments (e. g. LAMBDA_CONS(15)(20)
), it will return a function
that takes an argument l
and returns the l
call using the pair elements
as arguments (in our example, l(15)(20)
). That is: LAMBDA_CONS(15)(20) =
. In Python and other languages that supports first-class
lambda l: l(15)(20)
functions, those two values are stored in a
closure, and
we can even get them this way:
l = LAMBDA_CONS(15)(20)
a, b = (x.cell_contents for x in l.__closure__) # a = 15, b = 20
About LAMBDA_CAR
and LAMBDA_CDR
, they return the first and the
second element of the pair, respectively.
Mental exercise: try to understand why LAMBDA_CAR
and LAMBDA_CDR
work!
Lists
If you paid enough attention, you noted that car
, cdr
and cons
are the
same names that we used to define the functions that operate over lists. And
yes, they are the same! This happens because the way lists are implemented in
Church encoding.
Church lists just are pairs where:
- the first element is the first element of the list
- the second element is a list with the rest of the elements
That is a recursive definition where the base of the recursion (an empty
list) can be implemented by many ways. Here we are using to represent an empty
list the boolean LAMBDA_FALSE
:
LAMBDA_EMPTY = LAMBDA_FALSE
This way, a list with the values [1, 2, 3]
can be declared this way:
LAMBDA_CONS(1)(LAMBDA_CONS(2)(LAMBDA_CONS(3)(LAMBDA_FALSE)))
In practice, they are recursive pairs:
(1, (2, (3, LAMBDA_EMPTY)))
So many parentheses! But note that, at this point, LAMBDA_CAR
, LAMBDA_CDR
and LAMBDA_CONS
, when applied to lists have the same behaviour than car
,
cdr
and cons
that we defined to operate over Python lists:
LAMBDA_CAR
returns the first element of the first pair, this is, the first
element of the list (1
)LAMBDA_CDR
returns the first element of the second pair, this is, the
rest of the list ((2, (3, LAMBDA_EMPTY))
)LAMBDA_CONS
adds another pair, appending another element
As the definition of those lists is recursive, the iteration over them will also
be done in a recursive way. The function that we’ll use to know whether the
recursion has reached the end is this:
LAMBDA_ISEMPTY = lambda l: l(lambda h: lambda t: lambda d: LAMBDA_FALSE)(LAMBDA_TRUE)
That is:
- if
l
is empty (it is equal toLAMBDA_EMPTY
), then it returns the second
argument:LAMBDA_TRUE
- if
l
is not empty, thenl
is a pair.l
is called with the function(lambda h:
as argument. That function discards
lambda t: lambda d: LAMBDA_FALSE)
everything and returnLAMBDA_FALSE
. Try to simulate it ;-).
Conversion
Pairs can be converted from and to Python lists that have only two
elements. The pythonic way to do that would be using tuples of size 2, but, in
order to keep the code homogeneity, I’m using lists:
def l2p(l):
return [LAMBDA_CAR(l), LAMBDA_CDR(l)]
def p2l(p):
return LAMBDA_CONS(p[0])(p[1])
We can also convert Python lists and Church lists:
def ll2pl(l):
if l2b(LAMBDA_ISEMPTY(l)): return []
return [LAMBDA_CAR(l)] + ll2pl(LAMBDA_CDR(l))
def pl2ll(l):
if len(l) == 0: return LAMBDA_EMPTY
return LAMBDA_CONS(l[0])(pl2ll(l[1:]))
Using Church pairs in partition
As partition
returns two values (a Python tuple), we can use here a Church pair:
# before:
return L, R
# after:
return LAMBDA_CONS(L)(R)
Using Church lists in partition
Let’s use Church lists in quicksort! First of all, we’re going to convert
partition
to operate over Church lists. Currently, our situation is this:
def partition(A):
p = car(A)
L, R = [], []
for x in cdr(A):
if l2b(LAMBDA_LESS(x)(p)):
L, R = cons(x, L), R
else:
L, R = L, cons(x, R)
L, R = L, cons(p, R)
return L, R
So, we need to replace car
, cdr
, cons
and []
by their corresponding in
lambda calculus:
def partition(A):
p = LAMBDA_CAR(A)
L, R = LAMBDA_EMPTY, LAMBDA_EMPTY
for x in lliterator(LAMBDA_CDR(A)):
if l2b(LAMBDA_LESS(x)(p)):
L, R = LAMBDA_CONS(x)(L), R
else:
L, R = L, LAMBDA_CONS(x)(R)
L, R = L, LAMBDA_CONS(p)(R)
return LAMBDA_CONS(L)(R)
Note that I created the generator lliterator
to iterate over Church lists:
def lliterator(l):
while not l2b(LAMBDA_ISEMPTY(l)):
yield LAMBDA_CAR(l)
l = LAMBDA_CDR(l)
Using Church lists in quicksort
Now we’re going to add Church lists to the function quicksort
! We still
need to define the function concat
for Church lists. We can implement it
recursively:
- If the list on the left is empty, we need to use the list on the right
- If the list on the left is not empty, it returns a new list where:
- the first element is the first element (
car
) of the list on the left - the rest of the list is the concatenation of the rest (
cdr
) of the
list on the left with the list on the right
- the first element is the first element (
That is (note the currying):
def LAMBDA_CONCAT(l1):
def _LAMBDA_CONCAT(l2):
if l2b(LAMBDA_ISEMPTY(LAMBDA_CDR(l1))):
return LAMBDA_CONS(LAMBDA_CAR(l1))(l2)
else:
return LAMBDA_CONS(LAMBDA_CAR(l1))(LAMBDA_CONCAT(LAMBDA_CDR(l1))(l2))
return _LAMBDA_CONCAT
This is a little different to the other operations that were written in a single
expression. Spoiler: we’re going to handle this further!
Once having concat
defined, we can replace all the Python list operations by
Church list operations in quicksort
:
def quicksort(A):
# len(A) <= 1
if l2b(LAMBDA_ISEMPTY(A)): return A
if l2b(LAMBDA_ISEMPTY(LAMBDA_CDR(A))): return A
L, R = partition(A)
p = LAMBDA_CAR(R)
L = quicksort(L)
R = quicksort(LAMBDA_CDR(R))
return LAMBDA_IF(LAMBDA_ISEMPTY(L))(LAMBDA_CONS(p)(R))(LAMBDA_CONCAT(L)(LAMBDA_CONS(p)(R)))
You can see it here.
Replacing loops by recursive functions
As in lambda calculus there aren’t any states, we can’t use loops as we do
in imperative languages, that is, repeating a code snippet and changing the
state of a variable.
And due to the lack of states, instead of changing an existing value we return a
new value, just like we did when replacing the standard behaviour of
quicksort to return a sorted list instead of sort the original list.
Even when we are write code in languages that support both the functional and
imperative paradigms, like Python, if we restrict ourselves to write a code in a
functional manner we can’t use loops.
And how can we solve the problems that would be solved using loops? There are
many solutions depending on the case, for example, we could use reduce
, list
comprehensions, map
, filter
, recursive functions, etc. In this quicksort
we only have one loop, in partition
. We’re going to replace it by a
recursive function.
Replacing for
by while
Currently, the loop is this:
p = LAMBDA_CAR(A)
L, R = LAMBDA_EMPTY, LAMBDA_EMPTY
for x in lliterator(LAMBDA_CDR(A)):
if l2b(LAMBDA_LESS(x)(p)):
L, R = LAMBDA_CONS(x)(L), R
else:
L, R = L, LAMBDA_CONS(x)(R)
After removing the iterator lliterator
and replacing the for
by a while
,
we get this:
p = LAMBDA_CAR(A)
L, R = LAMBDA_EMPTY, LAMBDA_EMPTY
S = LAMBDA_CDR(A)
while True:
if l2b(LAMBDA_ISEMPTY(S)): break
x = LAMBDA_CAR(S)
if l2b(LAMBDA_LESS(x)(p)):
L, R = LAMBDA_CONS(x)(L), R
else:
L, R = L, LAMBDA_CONS(x)(R)
S = LAMBDA_CDR(S)
In other words: at first S
is equal to the input list without the first
element (the pivot p
). Each iteration of while
removes a value from S
and stores it in x
. If x < p
, we append x
to the beginning of L
, otherwise
we append to the end of R
. The stop condition is when the list S
is empty.
For now on we can identify the elements that will be useful for writing this
loop as a recursive function:
- the inputs
L
andR
that are, at first, empty Church lists; - the input
S
that is, at first, equal toLAMBDA_CDR(A)
; - the outputs, that are the values of
L
andR
at the end of the loop; - the stop condition, that are when
S
is empty.
Converting while
to recursion
Cool, what we have so far is enough to write a recursive function (that I’m
calling here _partition
), given the while
loop code:
p = LAMBDA_CAR(A)
L, R = LAMBDA_EMPTY, LAMBDA_EMPTY
S = LAMBDA_CDR(A)
def _partition(S, L, R):
if l2b(LAMBDA_ISEMPTY(S)): return L, R
x = LAMBDA_CAR(S)
if l2b(LAMBDA_LESS(x)(p)):
nL, nR = LAMBDA_CONS(x)(L), R
else:
nL, nR = L, LAMBDA_CONS(x)(R)
S = LAMBDA_CDR(S)
return _partition(S, nL, nR)
nL, nR = _partition(S, L, R)
Note that _partition
doesn’t change the state of the inputs L
and
R
. Instead of changing the original input data, it returns new data that
are stored in the variables nL
and nR
, that are the L
and R
arguments of
the next recursive call.
We can also make that function return Church pairs instead of tuples:
p = LAMBDA_CAR(A)
L, R = LAMBDA_EMPTY, LAMBDA_EMPTY
S = LAMBDA_CDR(A)
def _partition(S, L, R):
if l2b(LAMBDA_ISEMPTY(S)): return LAMBDA_CONS(L)(R)
x = LAMBDA_CAR(S)
if l2b(LAMBDA_LESS(x)(p)):
nL, nR = LAMBDA_CONS(x)(L), R
else:
nL, nR = L, LAMBDA_CONS(x)(R)
S = LAMBDA_CDR(S)
return _partition(S, L, R)
LR = _partition(S, L, R)
nL, nR = LAMBDA_CAR(LR), LAMBDA_CDR(LR)
You can see it here.
Replacing variables by let
s
A let expression allows us to
define a value to a variable inside a scope so that its value will never
be changed. In Python, this concept makes little sense, but it is implemented
in many ways by different languages. I’ll show you some of them.
Starting by Kotlin, let
is a method that can be called by any object so
we can assign a temporary name to it:
val x = 2.let { a ->
3.let { b ->
a + b * a
}
}
In Haskell, let
is an expression where the first half is the value
attribution and the second half is the expression that we want to evaluate:
x = let a = 2
b = 3
in a + b * a
In Hy (Python with Lisp syntax), it is very close to
Haskell: first we attribute the values to the variables, then we declare the
expression that will use them:
(setv x
(let [
a 2
b 3
]
(+ a (* b a))
)
)
(x
will be 8 in the three examples above)
That construction is a very common in functional languages, as their variables
have a fixed value inside a scope. In addition, they are easy to be written
using lambda calculus. We can write the example above like that:
def _f(a, b):
return a + b * a
x = _f(2, 3)
# or using lambda and currying:
x = (lambda a: lambda b: a + b * a)(2)(3)
Note that, like before, we are attributing a = 2
and b = 3
and then
calculating a + b * a
.
Our mission in this step is to put all the variables that are not argument
or constants in lets, so they could be used in lambda calculus.
For now on, the code will be very unreadable, but let’s focus in an example of
how that substitution is made. In the function _partition
that we defined
before, we’re going to replace x
by a let. By now, this function looks like
this:
def _partition(S, L, R):
if l2b(LAMBDA_ISEMPTY(S)): return LAMBDA_CONS(L)(R)
x = LAMBDA_CAR(S)
if l2b(LAMBDA_LESS(x)(p)):
nL, nR = LAMBDA_CONS(x)(L), R
else:
nL, nR = L, LAMBDA_CONS(x)(R)
S = LAMBDA_CDR(S)
return _partition(S, L, R)
The if
here only changes the values of L
and R
, we can write it as an
if-expression:
def _partition(S, L, R):
if l2b(LAMBDA_ISEMPTY(S)): return LAMBDA_CONS(L)(R)
x = LAMBDA_CAR(S)
nL, nR = (LAMBDA_CONS(x)(L), R) if l2b(LAMBDA_LESS(x)(p)) else (L, LAMBDA_CONS(x)(R))
S = LAMBDA_CDR(S)
return _partition(S, nL, nR)
We’re only evaluating x
in order to be used by that if-expression, so we can
use a let
here:
- attribution:
x = LAMBDA_CAR(S)
- expression: the if-expression that we just defined
To do that, let’s declare a new function called _partition2
that takes x
as
its argument and call it soon after, passing as the argument x = LAMBDA_CAR(S)
:
def _partition(S, L, R):
if l2b(LAMBDA_ISEMPTY(S)): return LAMBDA_CONS(L)(R)
def _partition2(x):
return (LAMBDA_CONS(x)(L), R) if l2b(LAMBDA_LESS(x)(p)) else (L, LAMBDA_CONS(x)(R))
nL, nR = _partition2(LAMBDA_CAR(S))
S = LAMBDA_CDR(S)
return _partition(S, nL, nR)
We have a let
! We can also replace the tuple by a Church pair:
def _partition(S, L, R):
if l2b(LAMBDA_ISEMPTY(S)): return LAMBDA_CONS(L)(R)
def _partition2(x):
return LAMBDA_CONS(LAMBDA_CONS(x)(L))(R) if l2b(LAMBDA_LESS(x)(p)) else LAMBDA_CONS(L)(LAMBDA_CONS(x)(R))
LR = _partition2(LAMBDA_CAR(S))
nL, nR = LAMBDA_CAR(LR), LAMBDA_CDR(LR)
return _partition(LAMBDA_CDR(S), nL, nR)
You can see it here.
Rewriting functions using lambda
At this point, having all the variables replaced by let, the partition
and
quicksort
functions don’t have variables anymore. They only have some if
s,
the return expression, and the definition of internal functions used by the
lets (and they have the same characteristics).
Take a look at this (yeah, only a look because this code is unreadable):
def quicksort(A):
if l2b(LAMBDA_ISEMPTY(A)): return A
if l2b(LAMBDA_ISEMPTY(LAMBDA_CDR(A))): return A
def _quicksort(A, LR):
return LAMBDA_IF(LAMBDA_ISEMPTY(quicksort(LAMBDA_CAR(LR))))(LAMBDA_CONS(LAMBDA_CAR(LAMBDA_CDR(LR)))(quicksort(LAMBDA_CDR(LAMBDA_CDR(LR)))))(LAMBDA_CONCAT(quicksort(LAMBDA_CAR(LR)))(LAMBDA_CONS(LAMBDA_CAR(LAMBDA_CDR(LR)))(quicksort(LAMBDA_CDR(LAMBDA_CDR(LR))))))
return _quicksort(A, partition(A))
def partition(A):
def _partition(S, L, R, p):
if l2b(LAMBDA_ISEMPTY(S)): return LAMBDA_CONS(L)(R)
def _partition2(x, L, R, p):
if l2b(LAMBDA_LESS(x)(p)): return LAMBDA_CONS(LAMBDA_CONS(x)(L))(R)
else: return LAMBDA_CONS(L)(LAMBDA_CONS(x)(R))
def _partition3(S, LR, p):
return _partition(LAMBDA_CDR(S), LAMBDA_CAR(LR), LAMBDA_CDR(LR), p)
return _partition3(S, _partition2(LAMBDA_CAR(S), L, R, p), p)
def _partition4(LR):
return LAMBDA_CONS(LAMBDA_CAR(LR))(LAMBDA_CONS(LAMBDA_CAR(A))(LAMBDA_CDR(LR)))
return _partition4(_partition(LAMBDA_CDR(A), LAMBDA_EMPTY, LAMBDA_EMPTY, LAMBDA_CAR(A)))
We can replace all the if
s by if-expressions and those if-expressions
by LAMBDA_IF
that we defined using Church booleans. Besides that, the
internal functions can be defined using lambda
instead of def
as they
only have the return expression. Now we have this awful code:
def quicksort(A):
_quicksort = lambda A: lambda LR: LAMBDA_CONCAT(quicksort(LAMBDA_CAR(LR)))(LAMBDA_CONS(LAMBDA_CAR(LAMBDA_CDR(LR)))(quicksort(LAMBDA_CDR(LAMBDA_CDR(LR)))))
_quicksort2 = lambda A: LAMBDA_IF(LAMBDA_ISEMPTY(A))(lambda A: A)(lambda A: LAMBDA_IF(LAMBDA_ISEMPTY(LAMBDA_CDR(A)))(A)(_quicksort(A)(partition(A))))
return _quicksort2(A)(A)
def partition(A):
_partition2 = lambda x: lambda L: lambda R: lambda p: LAMBDA_IF(LAMBDA_LESS(x)(p))(LAMBDA_CONS(LAMBDA_CONS(x)(L))(R))(LAMBDA_CONS(L)(LAMBDA_CONS(x)(R)))
_partition3 = lambda S: lambda LR: lambda p: _partition(LAMBDA_CDR(S))(LAMBDA_CAR(LR))(LAMBDA_CDR(LR))(p)
_partition = (lambda S: LAMBDA_IF(LAMBDA_ISEMPTY(S))(lambda L: lambda R: lambda p: LAMBDA_CONS(L)(R))(lambda L: lambda R: lambda p: _partition3(S)(_partition2(LAMBDA_CAR(S))(L)(R)(p))(p)))
_partition4 = (lambda A: lambda LR: LAMBDA_CONS(LAMBDA_CAR(LR))(LAMBDA_CONS(LAMBDA_CAR(A))(LAMBDA_CDR(LR))))(A)
return _partition4(_partition(LAMBDA_CDR(A))(LAMBDA_EMPTY)(LAMBDA_EMPTY)(LAMBDA_CAR(A)))
In this case, the internal functions (even though they are now variables) can be
constants. This way, they don’t need to be inside quicksort
and
partition
that could only have the return expression. Then, they could be
written using lambda
instead of def
:
_quicksort = lambda A: lambda LR: LAMBDA_CONCAT(quicksort(LAMBDA_CAR(LR)))(LAMBDA_CONS(LAMBDA_CAR(LAMBDA_CDR(LR)))(quicksort(LAMBDA_CDR(LAMBDA_CDR(LR)))))
_quicksort2 = lambda A: LAMBDA_IF(LAMBDA_ISEMPTY(A))(lambda A: A)(lambda A: LAMBDA_IF(LAMBDA_ISEMPTY(LAMBDA_CDR(A)))(A)(_quicksort(A)(partition(A))))
quicksort = lambda A: _quicksort2(A)(A)
_partition2 = lambda x: lambda L: lambda R: lambda p: LAMBDA_IF(LAMBDA_LESS(x)(p))(LAMBDA_CONS(LAMBDA_CONS(x)(L))(R))(LAMBDA_CONS(L)(LAMBDA_CONS(x)(R)))
_partition3 = lambda S: lambda LR: lambda p: _partition(LAMBDA_CDR(S))(LAMBDA_CAR(LR))(LAMBDA_CDR(LR))(p)
_partition = (lambda S: LAMBDA_IF(LAMBDA_ISEMPTY(S))(lambda L: lambda R: lambda p: LAMBDA_CONS(L)(R))(lambda L: lambda R: lambda p: _partition3(S)(_partition2(LAMBDA_CAR(S))(L)(R)(p))(p)))
_partition4 = (lambda A: lambda LR: LAMBDA_CONS(LAMBDA_CAR(LR))(LAMBDA_CONS(LAMBDA_CAR(A))(LAMBDA_CDR(LR))))
partition = lambda A:_partition4(A)(_partition(LAMBDA_CDR(A))(LAMBDA_EMPTY)(LAMBDA_EMPTY)(LAMBDA_CAR(A)))
Recursion and Y combinator
Some of those lambda functions are recursive:
quicksort
calls_quicksort2
that callsquicksort
_partition
calls_partition3
that calls_partition
However, a property of lambda calculus is that a function doesn’t need to have
a name. But how can a function reference itself without knowing its name? The
answer is Y Combinator.
To illustrate Y combinator, take a look at this factorial function:
def fac(n):
return 1 if n == 0 else n * fac(n-1)
# using lambda:
fac = lambda n: 1 if n == 0 else n * fac(n-1)
Then we can use Y combinator to replace the fac
call:
fac = (lambda f: f(f))(lambda f: lambda n: 1 if n == 0 else n * f(f)(n-1))
# we even don't need to name fac. This expression calculates 5! = 120 recursively:
(lambda f: f(f))(lambda f: lambda n: 1 if n == 0 else n * f(f)(n-1))(5)
What happens inside that thing? Note that we have a function (lambda f: lambda n: 1 if
, very similar to the original
n == 0 else n * f(f)(n-1))fac
, except that
it takes an argument f
and calls f(f)
instead of fac
. The idea of Y
combinator here is that f
will always be the same function and that it
passes itself as an argument, recursively, in order to the allow the
recursive calls to make another recursive calls. Who will guarantee the base
of that recursion is (lambda f: f(f))
, that will provide the first passing of
that function to itself.
Mental exercise: try to simulate fac(2)
and see the magic happening.
Using Y combinator
Ok, now we can replace the recursive call in quicksort
by a Y combinator. By
now it looks like this:
_quicksort2 = lambda A: LAMBDA_IF(LAMBDA_ISEMPTY(A))(lambda A: A)(lambda A: LAMBDA_IF(LAMBDA_ISEMPTY(LAMBDA_CDR(A)))(A)(_quicksort(A)(partition(A))))
quicksort = lambda A: _quicksort2(A)(A)
And if we replace it by Y combinator:
_quicksort2 = lambda r: lambda A: LAMBDA_IF(LAMBDA_ISEMPTY(A))(lambda A: A)(lambda A: LAMBDA_IF(LAMBDA_ISEMPTY(LAMBDA_CDR(A)))(A)(_quicksort(r)(A)(partition(A))))
quicksort = (lambda r: r(r)) lambda A: _quicksort2(r)(A)(A)
Mental exercise: the quicksort
call is not in quicksort
itself but in
_quicksort2
(that is called by quicksort
). Can you figure out how Y
combinator is used in that situation?
You can see it here.
Expanding everything!
At this point, all the values, data structures and if
s are
functions. Also, those functions and all the others are values that can be
written in a single expression.
Our work here is, basically, replace all the constants by their values
so that quicksort
can be a single expression. This can be done using the text
replacement tool of a text editor.
Finally we have this awful thing:
quicksort = (lambda r: r(r))(lambda r: lambda A: (lambda r: lambda A: (lambda c: lambda t: lambda e: c(t)(e))((lambda l: l(lambda h: lambda t: lambda d: (lambda a: lambda b: b))((lambda a: lambda b: a)))(A))(lambda A: A)(lambda A: (lambda c: lambda t: lambda e: c(t)(e))((lambda l: l(lambda h: lambda t: lambda d: (lambda a: lambda b: b))((lambda a: lambda b: a)))((lambda p: p(lambda a: lambda b: b))(A)))(A)((lambda r: lambda A: lambda LR: (lambda c: lambda t: lambda e: c(t)(e))((lambda l: l(lambda h: lambda t: lambda d: (lambda a: lambda b: b))((lambda a: lambda b: a)))(r(r)((lambda p: p(lambda a: lambda b: a))(LR))))((lambda a: lambda b: lambda l: l(a)(b))((lambda p: p(lambda a: lambda b: a))((lambda p: p(lambda a: lambda b: b))(LR)))(r(r)((lambda p: p(lambda a: lambda b: b))((lambda p: p(lambda a: lambda b: b))(LR)))))(((lambda r: r(r)) (lambda r: lambda l1: (lambda c: lambda t: lambda e: c(t)(e))((lambda l: l(lambda h: lambda t: lambda d: (lambda a: lambda b: b))((lambda a: lambda b: a)))((lambda p: p(lambda a: lambda b: b))(l1)))(lambda l2: (lambda a: lambda b: lambda l: l(a)(b))((lambda p: p(lambda a: lambda b: a))(l1))(l2))((lambda r: lambda l2: (lambda a: lambda b: lambda l: l(a)(b))((lambda p: p(lambda a: lambda b: a))(l1))(r(r)((lambda p: p(lambda a: lambda b: b))(l1))(l2)))(r))))(r(r)((lambda p: p(lambda a: lambda b: a))(LR)))((lambda a: lambda b: lambda l: l(a)(b))((lambda p: p(lambda a: lambda b: a))((lambda p: p(lambda a: lambda b: b))(LR)))(r(r)((lambda p: p(lambda a: lambda b: b))((lambda p: p(lambda a: lambda b: b))(LR)))))))(r)(A)((lambda A:((lambda A: lambda LR: (lambda a: lambda b: lambda l: l(a)(b))((lambda p: p(lambda a: lambda b: a))(LR))((lambda a: lambda b: lambda l: l(a)(b))((lambda p: p(lambda a: lambda b: a))(A))((lambda p: p(lambda a: lambda b: b))(LR)))))(A)(((lambda r: r(r))(lambda r: lambda S: (lambda c: lambda t: lambda e: c(t)(e))((lambda l: l(lambda h: lambda t: lambda d: (lambda a: lambda b: b))((lambda a: lambda b: a)))(S))(lambda L: lambda R: lambda p: (lambda a: lambda b: lambda l: l(a)(b))(L)(R))(lambda L: lambda R: lambda p: (lambda r: lambda S: lambda LR: lambda p: r(r)((lambda p: p(lambda a: lambda b: b))(S))((lambda p: p(lambda a: lambda b: a))(LR))((lambda p: p(lambda a: lambda b: b))(LR))(p))(r)(S)((lambda x: lambda L: lambda R: lambda p: (lambda c: lambda t: lambda e: c(t)(e))((lambda m: lambda n: (lambda a: lambda b: a(b)((lambda a: lambda b: b)))((lambda m: lambda n: (lambda n: n(lambda x: (lambda a: lambda b: b))((lambda a: lambda b: a)))((lambda m: lambda n: n((lambda n: lambda f: lambda x: n(lambda g: lambda h: h(g(f)))(lambda y: x)(lambda y: y)))(m))(m)(n)))(m)(n))((lambda a: a((lambda a: lambda b: b))((lambda a: lambda b: a)))((lambda m: lambda n: (lambda a: lambda b: a(b)((lambda a: lambda b: b)))((lambda m: lambda n: (lambda n: n(lambda x: (lambda a: lambda b: b))((lambda a: lambda b: a)))((lambda m: lambda n: n((lambda n: lambda f: lambda x: n(lambda g: lambda h: h(g(f)))(lambda y: x)(lambda y: y)))(m))(m)(n)))(m)(n))((lambda m: lambda n: (lambda n: n(lambda x: (lambda a: lambda b: b))((lambda a: lambda b: a)))((lambda m: lambda n: n((lambda n: lambda f: lambda x: n(lambda g: lambda h: h(g(f)))(lambda y: x)(lambda y: y)))(m))(m)(n)))(n)(m)))(m)(n))))(x)(p))((lambda a: lambda b: lambda l: l(a)(b))((lambda a: lambda b: lambda l: l(a)(b))(x)(L))(R))((lambda a: lambda b: lambda l: l(a)(b))(L)((lambda a: lambda b: lambda l: l(a)(b))(x)(R))))((lambda p: p(lambda a: lambda b: a))(S))(L)(R)(p))(p))))((lambda p: p(lambda a: lambda b: b))(A))((lambda a: lambda b: b))((lambda a: lambda b: b))((lambda p: p(lambda a: lambda b: a))(A))))(A)))))(r)(A)(A))
Using lambdasort
Of course we can’t use this quicksort
by itself in a list as it operates in
Church encoding. We’ll need a wrapper to translate the Python types to Church
encoding, sort the Church list using quicksort
and then translate it back to a
Python list. We’re going to use the previous functions.
def quicksort_wrapper(A):
church = pl2ll([i2l(x) for x in A])
sorted_church = quicksort(church)
return [l2i(x) for x in ll2pl(sorted_church)]
Now you can use quicksort_wrapper
to sort your list and it will use our
lambdasort as backend:
>>> from lambdasort import quicksort_wrapper
>>> x = [22, 33, 11, 55, 99, 11, 33, 77, 44]
>>> quicksort_wrapper(x)
[11, 11, 22, 33, 33, 44, 55, 77, 99]
Final thoughts
I wrote lambdasort in 2017 (my third year of university) in just two intense
days, after a class by Professor Gubi
about lambda calculus and Y
combinator. He told us about Programming with Nothing, mentioned earlier. I
found it so impressive that I wanted to do something similar, and challenged
myself to write something harder then a fizzbuzz, so here we are!
Write it was really fun, and I didn’t notice at first how much I learned in only
two days, and it took me years to finally write this text explaining what I
did. So, thank you for reading it!
Last but not least, English is not my first language and I’m trying to make my
best efforts to keep my personal page in both Portuguese and English. So, if you
find something wrong about it or about anything here feel free to
open a issue.