Quicksort written in Python only using lambdas!
GitHub: [https://github.com/lucasoshiro/lambdasort](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:
~~~python
# 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 has no value that isn't a function? For example:
~~~python
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`!
The famous **lambda calculus** is it. 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](https://en.wikipedia.org/wiki/Lambda_calculus).
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](https://www.cambridge.org/core/journals/journal-of-symbolic-logic/article/abs/computability-and-definability/FE8B4FC84276D7BACB8433BD578C6BFD#access-block)
itself has proven it!
The excellent [Programming with Nothing](https://tomstu.art/programming-with-nothing)
by Tom Stuard 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 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:
~~~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, but `cons` 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 calls `partition` to split the list that we want to
sort into two and **sorts** each one using `quicksort` 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](https://github.com/lucasoshiro/lambdasort/blob/simple-quicksort/lambdasort.py).
## 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)
Luckly, the creator of lambda calculus, **Alonzo Church** has also shown us how
to do that. There's an [article about it](https://en.wikipedia.org/wiki/Church_encoding)
on Wikipedia.
### Booleans
Let's start by the easier ones, **booleans**:
~~~python
#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:
b`, why there are two `lambdas`?". 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,
a3, ..., an)`. The name of that conversion is
**[currying](https://en.wikipedia.org/wiki/Currying)**, named after Haskell
Curry. An exemple 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 parameters `false` and
`true`. If the boolean is `true`, it returns the **first argument** (`false`);
if it is `false`, 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 of `or ` is `true`, then
it returns its **first argument** (`true`); if it is `false`, then it returns
its **second argument** (the second argument of `or`).
- `and`: much like `or`. Try to simulate it mentally ;-)
We can also define an `if`:
#### if
~~~python
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`:
~~~python
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`):
~~~python
#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](https://github.com/lucasoshiro/lambdasort/blob/booleans/lambdasort.py)
### Integers
**Chuch numerals** are defined as:
~~~python
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 like `false`)
- 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(...)`:
~~~python
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:
~~~python
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:
~~~python
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?):
~~~python
LAMBDA_EQZ = lambda n: n(lambda x: LAMBDA_FALSE)(LAMBDA_TRUE)
~~~
Using only the operation of **equals zero**, combined with **boolean** and
**aritmetic** operations we can define the others:
- `m <= n`: `(m - n) == 0` (remember: `m - n = 0` if `n > m`)
- `m == n`: `(m <= n) and (n <= m)`
- `n < m`: `(m <= n) and not (m == n)`
Or, in Python / lambda calculus:
~~~python
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:
~~~python
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:
~~~python
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:
~~~python
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 Chuch numbers, we can apply the first change to Quicksort:
~~~python
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` funcion 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](https://github.com/lucasoshiro/lambdasort/blob/integers/lambdasort.py).
### 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:
~~~python
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) =
lambda l: l(15)(20)`. In Python and other languages that supports first-class
functions, those two values are stored in a
**[closure](https://en.wikipedia.org/wiki/Closure_(computer_programming))**, and
we can even get them this way:
~~~python
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`:
~~~python
LAMBDA_EMPTY = LAMBDA_FALSE
~~~
This way, a list with the values `[1, 2, 3]` can be declared this way:
~~~python
LAMBDA_CONS(1)(LAMBDA_CONS(2)(LAMBDA_CONS(3)(LAMBDA_FALSE)))
~~~
In practice, they are recursive pairs:
~~~python
(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:
~~~python
LAMBDA_ISEMPTY = lambda l: l(lambda h: lambda t: lambda d: LAMBDA_FALSE)(LAMBDA_TRUE)
~~~
That is:
- if `l` is **empty** (it is equal to `LAMBDA_EMPTY`), then it returns the second
argument: `LAMBDA_TRUE`
- if `l` is **not empty**, then `l` is a pair. `l` is called with the function `(lambda h:
lambda t: lambda d: LAMBDA_FALSE)` as argument. That function discards
everything and return `LAMBDA_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:
~~~python
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:
~~~python
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:
~~~python
# 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 Chuch lists. Currently, our situation is this:
~~~python
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:
~~~python
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:
~~~python
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**
That is (note the currying):
~~~python
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`:
~~~python
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](https://github.com/lucasoshiro/lambdasort/blob/pairs-lists/lambdasort.py).
## 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:
~~~python
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:
~~~python
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 recusive function:
- the inputs **`L` and `R`** that are, at first, empty Church lists;
- the input **`S`** that is, at first, equal to `LAMBDA_CDR(A)`;
- the **outputs**, that are the values of `L` and `R` 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:
~~~python
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:
~~~python
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]((https://github.com/lucasoshiro/lambdasort/blob/recursion/lambdasort.py).).
## Replacing variables by `let`s
A [let expression](https://en.wikipedia.org/wiki/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:
~~~kotlin
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:
~~~haskell
x = let a = 2
b = 3
in a + b * a
~~~
In **[Hy](http://hylang.org)** (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:
~~~hy
(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:
~~~python
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:
~~~python
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**:
~~~python
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)`:
~~~python
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**:
~~~python
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](https://github.com/lucasoshiro/lambdasort/blob/let/lambdasort.py).
## 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):
~~~python
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:
~~~python
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`:
~~~python
_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 calls `quicksort`
- `_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](https://en.wikipedia.org/wiki/Fixed-point_combinator#Fixed-point_combinators_in_lambda_calculus)**.
To illustrate Y combinator, take a look at this factorial function:
~~~python
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:
~~~python
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
n == 0 else n * f(f)(n-1))`, very similar to the original `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 simlate `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:
~~~python
_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:
~~~python
_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](https://github.com/lucasoshiro/lambdasort/blob/y-combinator/lambdasort.py).
## 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.
~~~python
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:
~~~python
>>> 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](https://memorial.ime.usp.br/homenageados/5)
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](https://github.com/lucasoshiro/lucasoshiro.github.io/issues).