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?
Arithmetic
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).
Hybrid
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!
Projections
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.
(see also a Forth/postscript-like stack language DSL in bruijn using this stack encoding here)
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.
Wadsworth, Christopher. 1980. “Some Unusual -Calculus Numeral Systems.” To HB Curry: Essays on Combinatory Logic, Lambda Calculus; Formalism.