Uncategorized

Lambdasort – Lucas Seiki Oshiro



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, 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.

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:
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, 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 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

LAMBDA_IF = lambda c: lambda t: lambda e: c(t)(e)

In if, cis 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 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(...):

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 if n > 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) =
lambda l: l(15)(20)
. In Python and other languages that supports first-class
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 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:

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

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 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:

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 lets

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 ifs,
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 ifs 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 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.

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
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 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 ifs 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.



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *