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:
⟨n⟩d=λS(n)n
For example, the decimal number
4
would be encoded as
⟨4⟩d=λ54.
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
⟨n⟩e=λknEn,
with
En
being a normal form and
kn
increasing indefinitely with
n.
And, to be fair, it makes sense: An increasing
n
will require an increasing number of applications scaling with
kn
– 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
succ=λ321=bk
Proof
To prove:
∀n∈N0:succ⟨n⟩d⇝∗⟨S(n)⟩d
Proof by
β-reduction
(⇝):
succ⟨n⟩d=⇝⇝(λ321)λS(n)nλ2(λS(n)n)1λ2λnS(n)=λS(S(n))S(n)=⟨S(n)⟩d
The pred function can be implemented similarly:
pred=λ2100=w
Proof
To prove:
∀n∈N0:pred⟨S(n)⟩d⇝∗⟨n⟩d
Proof by
β-reduction
(⇝):
pred⟨S(n)⟩d=⇝⇝⇝(λ2100)λS(S(n))S(n)λ(λS(S(n))S(n))00λ(λS(n)S(n))0λλnn=λS(n)n=⟨n⟩d
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
⟨0⟩d.
Optimally, pred should probably return
λ10
again, but in this case the
ω
combinator
λ100
is returned. I’m not sure if there exists a totalpred 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:
add=λ32(10)=b
Proof
To prove:
∀a,b∈N0:add⟨a⟩d⟨b⟩d⇝∗⟨a+b⟩d
Proof by
β-reduction
(⇝):
add⟨a⟩d⟨b⟩d=⇝⇝⇝⇝(λ32(10))(λS(a)a)λS(b)b(λ2(λS(a)a)(10))λS(b)bλ1(λS(a)a)(λS(b)b0)λ1(λS(a)a)(λbb)λ1λaλb[a+b]=λS(a+b)[a+b]=⟨a+b⟩d
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:
⟨n⟩c=λ10∘⋯∘0n
Converting (“apostatizing”) a Church numeral to a de Bruijn numeral
is done using the conv function:
conv=λ0k=tk
With the internal structure of the Church encoding, this basically
comes down to applying succn−1
times on
k=⟨1⟩d,
as
succ=bk.
Proof
To prove:
∀n∈N0:conv⟨n⟩c⇝∗⟨n⟩d
Proof by beta reduction
(⇝):
conv⟨n⟩c=⇝⇝⇝succ(λ0k)λ0∘⋯∘0n(λ0∘⋯∘0n)kk∘⋯∘kn=bk(bk(…(bk⟨1⟩d)…)λS(n)n=⟨n⟩d
In general, assuming an encoding
e
of
⟨n⟩e
has a zero?* and pred* function, a universal
conversion function conv* can be defined:
conv*=fix(λ3zero?*01(2(succ1)(pred*0)))⟨0⟩d
The function consists of fairly typical fix-recursion
using a left case
(zero?01)
to return the constructed term if the input number is zero and a right
case
(2(succ1)(pred*0))
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
⟨a⟩d=k∘⋯∘ka.
If we now, say, want to multiply a number with a different number
b,
this composition sequence has to be composed
b-times.
mult⟨a⟩d⟨a⟩d⇝∗k∘⋯∘ka∘⋯∘k∘⋯∘kab
And – you might have guessed it – the
n-times
composing Church encoding does exactly that:
⟨a⟩c⟨b⟩d⇝∗⟨a⋅b⟩d
Indeed, any operation on Church numerals can now be made to return a
de Bruijn numeral, simply by applying
⟨1⟩d.
Others can be transformed internally such that they also use de Bruijn
operations:
fac=λ10(λ20(1(λ221(10))))(λ1⟨1⟩d)ifac⟨5⟩c⇝∗⟨120⟩d
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
n-tuples
really hard to work with. This is because selecting the
ith
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:
tuplen=λS(n)0n[n−1]…1[x1,x2,…,xn]c=tuplenx1…xn⇝∗λ10x1x2…xn
Retrieving the
ith
element of an
n-tuple
would then look something like this:
selectni[x1,x2,…,xn]c⇝∗xi,
which of course must reject all terms but
xi,
therefore requiring an application of
λS(n)i
to the tuple. Specifically, with Church numerals as parameters,
selector⟨n⟩c⟨i⟩c⇝∗λS(n)i.
Luckily, such a term can be constructed using de Bruijn numerals. The
idea is simple: First, we apply
⟨i⟩d
to
⟨n⟩d,
such that we get
λnλS(i)i.
Then, we remove
i
abstractions such that it yields
λS(n)i.
Using i to pop the abstractions, we get:
selector=λ20(ti)(1k(0k))
(note that
⟨n⟩c(ta)⇝∗λ10aa…an)
Finally, with this knowledge, Church
n-tuples
can be used almost like Church lists. Some further functions:
pushpop0pop1popnmove=λ31(02)=q’’=λ21(λ11)=λ21(λ221)=λ31(2bk0)=λ31(λ13b(λ101)1)
where move moves the first element to the
nth
position, without knowing the size of the tuple!
move⟨i⟩c[x1,…,xn]⇝∗[x2,…,x1,…xn]
Notably, something like move or pop-n
generally requires recursion for Church lists. At the same time for
Church
n-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)
n
applied arguments.
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.