The beauty of mixed-radix bases
On the relationship between mixed-radix bases and array indexing
Numbers. They turn out to be pretty crucial, and everyone is familiar with the decimal number system, or base 101. Programmers, of course, also tend to use decimal numbers, but binary and hexadecimal also see frequent use (bases 2 and 16, respectively). Occasionally one even comes across octal (base 8).
Programming languages generally support writing numbers in several of the
above, sometimes following the C convention of using a 0
prefix for octal
numbers and 0x
for hexadecimal numbers. Also common are functions to
convert integers to and from different bases, usually represented as a text
string2.
There are a number of problems with using text strings to represent a
number. For instance, such functions are usually restricted to bases
2 … 36 because only [0-9a-z]
are available as digits.
They are also quite annoying to manipulate, and fairly fragile (what if the
string doesn't follow the expected syntax?). Another representation would
be as a vector of integers, each element corresponding to one digit: there
is no need to represent the digits by characters, so you have no upper bound
on what base to support (other than restrictions on the integer type
itself).
Languages in the APL family (at least APL, J, K) tend to take this approach, and in addition they also extend the “base” parameter to support vectors. That is, instead of using the same base for every position (the way decimal, binary etc work), we supply a vector with one base for each position. This might seem really weird at first, but is how a mixed-radix base works.
Why would we ever want different positions to take on different bases? One
everyday example would be representing time as days, hours, minutes and
seconds: there are 60 seconds to a minute, and 60 minutes to an hour, but of
course there are 24 hours to a day. Thus, our radix is ∞ 24 60 60
.
The rest of this article will explore a (hopefully) surprising and interesting use of these conversion functions, making an argument along the way in favour of zero-indexing for arrays, and finally provide a practical example of this in the J programming language.
We will return to mixed-radix bases in a moment, but first I want to briefly introduce the other party of this story: the array. In particular I want to introduce you to rectangular arrays, multidimensional arrays that conceptually can be thought of as a rectangle (or cuboid, or …), where each axis has a given length. Together, the length along each axis (from highest dimension to lowest) gives us the shape of the array, and the length of the shape is its rank (or number of dimensions). Arrays of rank 1 are typically called vectors.
Arrays of higher dimensions (such as matrices) can be stored in a lot of different ways. The by far most common approach today is row-major order, in which you read out the matrix in the order that you would read English text: left-to-right, then top-to-bottom. Elements adjacent in a row are stored adjacent in memory, but elements adjacent in a column are stored far apart (one matrix-width apart, in fact). The complementary approach, favouring columns, would be column-major order, notably seen in Fortran. There are other interesting approaches too3, but we will focus on only row-major order, which is what you see in C.
Another common example of a matrix in row-major order would be framebuffers
for a screen. In fact, a framebuffer is essentially a rank-3 array: its
shape would be height width depth
, where depth is the number of channels
(often 3 or 4, for red, green, blue, and possibly alpha).
Anyone who has ever worked with 2D graphics has probably written an expression of the form y⋅width + x or (y⋅width + x)⋅depth + channel to manipulate a particular pixel at some particular coordinate.
Let us take a moment and think about what we are doing here: we are indexing
a rectangular array. Consider the example with colour depth, whose shape is
height width depth
: we are indexing it at coordinate (y, x, channel),
yielding the expression (y⋅width + x)⋅depth + channel. This is,
inherently, treating the number (y, x, channel) as a base-height width
depth
number, converting it to a single integer.
Which is to say, indexing a multidimensional array is essentially radix conversion. This isn't restricted to two or three dimensions, of course, but works for any multidimensional array laid out in row-major order.
We can look at this from a slightly different angle too: nested loops in an imperative language. Consider nested loops as shown below.
for (int i = 0; i < I; i++) {
for (int j = 0; j < J; j++) {
for (int k = 0; k < K; k++) {
...
}
}
}
(Of course, one common use of such nested loops is to iterate along the axes of a rectangular array… so you might already see where this is heading.)
This isn't too bad in the case above with only three loops, but it is easy to see why this wouldn't look nice with more deeply-nested loops. Sometimes one doesn't even know the depth ahead of time; the number of nested loops needed might only be known at runtime. While it is possible to solve this recursively, it quickly gets very ugly.
So, what can we do instead? Well, in the above case the total number of
iterations will be I⋅J⋅K, so could we maybe collapse the loops into a
single one, looping from 0 to I⋅J⋅K? The answer is yes, and to extract
the individual i,j,k values we… perform a base conversion! In this case
we're going in the other direction, from a single compound number to its
representation in base I J K
.
(As an aside, this also makes a bit of an argument in favour of indexing arrays from 0 rather than 1: a decimal number's digits takes on values 0 … 9, and we have shown how rectangular array indexing behave very similarly to mixed-radix numbers, so it follows fairly naturally to have array indices be indexed from 0 as well. It just makes the mathematical relationships more elegant.)
Finally, I want to show an example of making use of
base-conversion-as-array-indexing in practice, from the source that first
taught me about this relationship. This is a definition in J from Ben
Gorte, generalising the built-in I.
to higher dimensions. J has a
fairly… unconventional syntax, but I will focus on explaining the semantics
of what is going on here.
Idot =: $ #: I.@:,
First, we need to know what I.
does in the first place. It is quite
simple: given a boolean vector (booleans in J are represented by 0 and 1 for
false and true, respectively), return the indices of all the 1's in
ascending order. Thus, I. 0 1 1 0 0 1 0 0
produces 1 2 5
(the elements
being zero-indexed, starting from the left).
One useful way of generalising this to higher dimensions would be to produce
a list of vectors, each vector giving the coordinate for a 1.
Unfortunately, for a variety of reasons this isn't how I.
generalises, so
we will have to implement it ourself. This is what Idot
, defined above,
is for.
Next, I will go through how this works step-by-step. The definition of
Idot
hinges on the fact that we already have I.
that works adequately
for vectors, so we want to make use of this and do some pre- and
post-processing to get the appropriate coordinates-of-ones as the result of
Idot
.
For preprocessing, we need to somehow turn the input array into a vector.
We do this by ravelling the array, essentially reading out its elements in
row-major order (the way it is laid out in memory). In practice, since the
data as it is in memory is already laid out properly, we only really need
to change the metadata. In C terms we essentially cast to int *
. After
this, we can apply I.
and get the indices of the ones in the ravelled
array.
Finally we need to postprocess the indices and turn them into vector
coordinates. This is of course a base conversion, making use of the
connection between array indexing and base conversion: we use antibase
(#:
) to expand each scalar into a vector, the radix of the conversion
being the shape ($
) of the original array. And thus, we have the result
we wanted!
Below is a REPL transcript of how this works out for a concrete example.
NB. index vector: increasing integers assembled into shape 2 3 4
i. 2 3 4
0 1 2 3
4 5 6 7
8 9 10 11
12 13 14 15
16 17 18 19
20 21 22 23
NB. pick out only the values 5 and 18, define this boolean array as `array`
] array =. 5 18 e.~ i. 2 3 4
0 0 0 0
0 1 0 0
0 0 0 0
0 0 0 0
0 0 1 0
0 0 0 0
, array NB. ravel array
0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0
I. , array NB. indices of 1's in ravelled array
5 18
$ array NB. shape of array
2 3 4
($ array) #: I. , array NB. use shape as radix in a base conversion
0 1 1
1 1 2
($ #: I.@,) array NB. same thing, expressed tacitly
0 1 1
1 1 2
The main takeaway of this article is that base conversion can crop up in the most surprising of places. I happened to find the connection to rectangular arrays particularly elegant and surprising, and so decided to focus on it. If you can think of other interesting, surprising uses of radix conversion (both of constant and mixed radix), I would love to hear about them!
- Yes, I am well aware that every base is base 10 :) ↩
- See
parseInt
/toString
in Java, JavaScript, C#;strtol
in C; etc. ↩ - One interesting approach is to store the elements as you would enumerate them along antidiagonals (top-right to bottom-left), starting with the top-left element, then the two elements neighbouring it, and so on. This approach makes it a bit more involved to index a particular element, but one benefit is that it is possible to (lazily) store arrays that are infinite along one or several axes. ↩