assumptions in this post
- understanding of pure lambda calculus
- zero-based de Bruijn
indices instead of named variables
consecutive lambdas are written as
encodes
as a lambda calculus term of encoding
is the Peano successor function
There are many ways to encode numbers and do arithmetic in the pure
lambda calculus, for example using unary Church
numerals, various -ary
representations, and the Scott
encoding.
A method that I have not seen yet is an encoding by reference
depth. Specifically, I refer to a nested de Bruijn index that, by
itself, encodes the number:
For example, the decimal number
would be encoded as
.
Isn’t this just so elegant? Because I could not find a name for the
encoding, I call them de Bruijn numerals.
However, during my experiments with this encoding, I made an
unfortunate discovery. While reading up on the theory behind optimal
reduction in some ancient book I found in the library, I stumbled upon
the work “Some Unusual Numeral Systems” by Wadsworth (1980), which was
coincidentally published in the same book as Lévy’s legendary paper on
optimal reduction. In this work, Christopher Wadsworth analyzed
different properties of numeral systems and the requirements they have
to fulfill to be useful for arithmetic.
Specifically, he calls a numeral system adequate if it
allows for a successor (succ) function, predecessor
(pred) function, and a zero? function yielding
a true (false) encoding when a number is zero
(or not). He then went on to show that there can not exist an adequate
numeral system encoding numbers as
with
being a normal form and
increasing indefinitely with
.
And, to be fair, it makes sense: An increasing
will require an increasing number of applications scaling with
– thus requiring a zero? function to know the
number already. Otherwise, the increasing abstractions can not be
“collapsed” to terms encoding booleans, as they must have a constant
number of abstractions. (read his paper
for more details)
Unfortunately, a zero? function is required in order to
convert any number from this numeral system to another system. So, even
with these limitations, can the “de Bruijn numerals” still be useful for
anything? What do other arithmetic operations look like?
The simplest operation is the succ function, which is
minimally defined as
Proof
To prove:
Proof by
-reduction
():
The pred function can be implemented similarly:
Proof
To prove:
Proof by
-reduction
():
They both use the fact that the outermost abstraction is always bound
to the inner de Bruijn index. Substituting the index with a reference to
a fresh abstraction results in incrementing/decrementing behavior
(utilizing the shift procedure of
-reduction
with de Bruijn indices)
One problem of pred is its application to
.
Optimally, pred should probably return
again, but in this case the
combinator
is returned. I’m not sure if there exists a total
pred function for this encoding.
Now, normally further operations could be defined by recursion ending
with a truthy zero?. Of course, this is not an option for
this encoding.
So instead, here’s an incredibly tiny self-contained addition
function:
Proof
To prove:
Proof by
-reduction
():
Isn’t it magical? This is one of the reasons I still like the
encoding: it abuses the de Bruijn shifting so elegantly! It’s basically
like a metacircular numeral system since it hijacks the host
reducer’s computations!
On the other hand, I was not yet able to come up with further such
elegant functions (maybe you can help?) – instead, I came up with some
hybrid implementations which use multiple different numeral
systems (in this case, Church’s).
A Church numeral is not that different from a de Bruijn numeral, as
it turns out. It is defined like this:
Converting (“apostatizing”) a Church numeral to a de Bruijn numeral
is done using the conv function:
With the internal structure of the Church encoding, this basically
comes down to applying succ
times on
,
as
.
Proof
To prove:
Proof by beta reduction
():
In general, assuming an encoding
of
has a zero?* and pred* function, a universal
conversion function conv* can be defined:
The function consists of fairly typical fix-recursion
using a left case
to return the constructed term if the input number is zero and a right
case
that calls the fix-induced function recursively with the
incremented de Bruijn numeral and decremented input number.
Anyway, with the insight on Church numerals, we find that the de
Bruijn numerals are defined from nothing else than
If we now, say, want to multiply a number with a different number
,
this composition sequence has to be composed
-times.
And – you might have guessed it – the
-times
composing Church encoding does exactly that:
Indeed, any operation on Church numerals can now be made to return a
de Bruijn numeral, simply by applying
.
Others can be transformed internally such that they also use de Bruijn
operations:
Of course – still – the resulting de Bruijn numerals can never be
converted back to an encoding like Church’s.
Next, an actual problem where I use de Bruijn numerals in practice
all the time!
I always found Church
-tuples
really hard to work with. This is because selecting the
th
element – again – requires an increasing number of abstractions! In fact
their behavior is so similar that I could only now make use of them
after gaining insight about de Bruijn numerals. They are defined like
this:
Retrieving the
th
element of an
-tuple
would then look something like this:
which of course must reject all terms but
,
therefore requiring an application of
to the tuple. Specifically, with Church numerals as parameters,
Luckily, such a term can be constructed using de Bruijn numerals. The
idea is simple: First, we apply
to
,
such that we get
.
Then, we remove
abstractions such that it yields
.
Using i to pop the abstractions, we get:
(note that
)
Finally, with this knowledge, Church
-tuples
can be used almost like Church lists. Some further functions:
where move moves the first element to the
th
position, without knowing the size of the tuple!
Notably, something like move or pop-n
generally requires recursion for Church lists. At the same time for
Church
-tuples,
something as simple as counting the elements is not possible for the
same reasons as a zero? function for de Bruijn numerals, as
the counting function would have to ignore (and count)
applied arguments.
All presented definitions can be found in bruijn’s standard library:
std/Number/Bruijn,
std/Tuples.
Thanks for reading. Please let me know if you know of – or come up
with! – other/better operations for de Bruijn numerals. Contact me via
email. Support on Ko-fi. Subscribe on RSS. Follow on Mastodon.
-Calculus
Numeral Systems.” To HB Curry: Essays on Combinatory Logic,
Lambda Calculus; Formalism.