import number-number from from-wordpress

This commit is contained in:
queue-miscreant 2025-07-22 19:32:47 -05:00
parent cb0f669874
commit cd4d3e57ec
2 changed files with 987 additions and 0 deletions

View File

@ -0,0 +1,479 @@
---
format:
html:
html-math-method: katex
---
Numbering Numbers: From 0 to ∞
==============================
The infinite is replete with paradoxes. Some of the best come from comparing sizes of infinite collections. For example, every natural number can be mapped to a (nonnegative) even number and vice versa.
$$
\N \rightarrow 2\N \\
n \mapsto 2n \\ ~ \\
0 \mapsto 0,~ 1 \mapsto2,~ 2 \mapsto 4,~ 3 \mapsto 6,~ 4 \mapsto 8, ...
$$
(For the purposes of this post, $0 \in \N, ~ 0 \notin \N^+$)
All even numbers are "hit" by this map (by the definition of an even number), and no two natural numbers map to the same even number (again, more or less by definition, since $2m = 2n$ implies that $m = n$ over $\N$). Therefore, the map is [one-to-one](https://en.wikipedia.org/wiki/Injective_function) and [onto](https://en.wikipedia.org/wiki/Surjective_function), and the map is a [bijection](https://en.wikipedia.org/wiki/Bijection). A consequence is that the map has an inverse, namely by reversing all of the arrows in the above block (i.e., the action of halving an even number).
Bijections with the natural numbers are easier to understand as a way to place things into a linear sequence. In other words, they enumerate some "sort" of item; in this case, even numbers.
In the finite world, a bijection between two things implies that they have the same size. It makes sense to extend the same logic to the infinite world, but there's a catch. The nonnegative even numbers are clearly a strict subset of the natural numbers, but by this argument they have the same size.
$$
\begin{matrix}
2\N & \longleftrightarrow & \N & \hookleftarrow & 2\N \\
0 & \mapsto & \textcolor{red}0 & \dashleftarrow & \textcolor{red}0 \\
2 & \mapsto & 1 & & \\
4 & \mapsto & \textcolor{red}2 & \dashleftarrow & \textcolor{red}2 \\
6 & \mapsto & 3 & & \\
8 & \mapsto & \textcolor{red}4 & \dashleftarrow & \textcolor{red}4 \\
10 & \mapsto & 5 & & \\
12 & \mapsto & \textcolor{red}6 & \dashleftarrow & \textcolor{red}6 \\
14 & \mapsto & 7 & & \\
16 & \mapsto & \textcolor{red}8 & \dashleftarrow & \textcolor{red}8 \\
\vdots & & \vdots & & \vdots
\end{matrix}
$$
Are we Positive?
----------------
The confusion continues if we look at the integers and the naturals. Integers are the natural numbers and their negatives, so it would be intuitive to assume that there are twice as many of them as there are naturals (more or less one to account for zero). But that logic fails for the naturals and the even numbers, and indeed, it fails for the integers and the naturals as well.
$$
\begin{align*}
\mathbb{N} &\rightarrow \mathbb{Z} \\
n &\mapsto \left\{ \begin{matrix}
n/2 & n \text{ even} \\
-(n+1)/2 & n \text{ odd}
\end{matrix} \right.
\end{align*}
\\ ~ \\
0 \mapsto 0,\quad 2 \mapsto 1, \quad 4 \mapsto 2, \quad 6 \mapsto 3, \quad 8 \mapsto 4,~... \\
1 \mapsto -1, \quad 3 \mapsto -2, \quad 5 \mapsto -3, \quad 7 \mapsto -4, \quad 9 \mapsto -5, ...
$$
Or, in Haskell (if you cover your eyes and pretend that the `undefined` below will never happen):
```{.haskell}
type Nat = Int
listIntegers :: Nat -> Int
listIntegers n
| n < 0 = undefined
| even n = n `div` 2
| otherwise = -(n + 1) `div` 2
```
In other words, this map sends even numbers to the naturals (the inverse of the doubling map) and the odds to the negatives. The same arguments about the bijective nature of this map apply as before, and so the paradox persists, since naturals are also a strict subset of integers.
### Rational Numbers
Rationals are a bit worse. To make things a little easier, let's focus on the positive rationals (i.e., fractions excluding 0). Unlike the integers, there is no obvious "next rational" after (or even before) 1. If there were, we could follow it with its reciprocal, like how an integer is followed by is negative in the map above.
On the other hand, the integers provide a sliver of hope that listing all rational numbers is possible. Integers can be defined as pairs of natural numbers, along with a way of considering two pairs equal.
$$
-1 = (0,1) \sim_\Z (1,2) \sim_\Z (2,3) \sim_\Z (3,4) \sim_\Z ... \\
~ \\
(a,b) \sim_\mathbb{Z} (c,d) \iff a+d = b+c \quad a,b,c,d \in \mathbb{N} \\
~ \\
\mathbb{Z} := ( \mathbb{N} \times \mathbb{N} ) / \sim_\mathbb{Z}
$$
```{.haskell}
intEqual :: (Nat, Nat) -> (Nat, Nat) -> Bool
intEqual (a, b) (c, d) = a + d == b + c
```
This relation is the same as saying $a - b = c - d$ (i.e., that -1 = 0 - 1, etc.), but has the benefit of not needing to define subtraction first. This is all the better, since, as grade-schoolers are taught, subtracting a larger natural number from a smaller one is impossible.
The same equivalence definition exists for positive rationals. It is perhaps more familiar, because of the emphasis placed on simplifying fractions when learning them. We can [cross-multiply](https://en.wikipedia.org/wiki/Cross-multiplication) fractions to get a similar equality condition to the one for integers.
$$
{1 \over 2} = (1,2) \sim_\mathbb{Q} \overset{2/4}{(2,4)} \sim_\mathbb{Q} \overset{3/6}{(3,6)} \sim_\mathbb{Q} \overset{4/8}{(4,8)} \sim_\mathbb{Q} ... \\
~ \\
(a,b) \sim_\mathbb{Q} (c,d) \iff ad = bc \quad a,b,c,d \in \mathbb{N}^+ \\
~ \\
\mathbb{Q^+} := ( \mathbb{N^+} \times \mathbb{N^+} ) / \sim_\mathbb{Q}
$$
```{.haskell}
ratEqual :: (Nat, Nat) -> (Nat, Nat) -> Bool
ratEqual (a, b) (c, d) = a * d == b * c
```
We specify that neither element of the pair can be zero, so this excludes divisions by zero (and the especially tricky case of 0/0, which would be equal to all fractions). Effectively, this just replaces where addition appears in the integer equivalence with multiplication.
### Eliminating Repeats
Naively, to tackle both of these cases, we might consider enumerating pairs of natural numbers. We order them by sums and break ties by sorting on the first index.
| Index | Pair (*a*, *b*) | Sum (*a* + *b*) | Integer (*a* - *b*) | Rational (*a*+1 / *b*+1) |
|-------|-----------------|-----------------|---------------------|--------------------------|
| 0 | (0, 0) | 0 | 0 | 1/1 |
| 1 | (0, 1) | 1 | -1 | 1/2 |
| 2 | (1, 0) | 1 | 1 | 2/1 |
| 3 | (0, 2) | 2 | -2 | 1/3 |
| 4 | (1, 1) | 2 | 0 | 2/2 = 1/1 |
| 5 | (2, 0) | 2 | 2 | 3/1 |
| 6 | (0, 3) | 3 | -3 | 1/4 |
| 7 | (1, 2) | 3 | -1 | 2/3 |
| 8 | (2, 1) | 3 | 1 | 3/2 |
| 9 | (3, 0) | 3 | 3 | 4/1 |
| ... | ... | ... | ... | ... |
```{.haskell}
-- All pairs of natural numbers that sum to n
listPairs :: Nat -> [(Nat, Nat)]
listPairs n = [ (k, n - k) | k <- [0..n] ]
-- Use a natural number to index the enumeration of all pairs
allPairs :: [(Nat, Nat)]
allPairs = concat $ map listPairs [0..]
allPairsMap :: Nat -> (Nat, Nat)
allPairsMap n = allPairs !! n
```
This certainly works to show that naturals and pairs of naturals can be put into bijection, but it when interpreting the results as integers or rationals, we double-count several of them. This is easy to see in the case of the integers, but it will also happen in the rationals. For example, the pair (3, 5) would correspond to 4/6 = 2/3, which has already been counted.
Incidentally, Haskell comes with a function called `nubBy`. This function eliminates duplicates according to another function of our choosing. We can also just implement it ourselves and use it to create a naive enumeration of integers and rationals, based on the equalities defined earlier:
```{.haskell}
nubBy :: (a -> a -> Bool) -> [a] -> [a]
nubBy f xs = nubBy' [] xs where
nubBy' ys [] = []
nubBy' ys (z:zs)
-- Ignore this element, something equivalent is in ys
| any (f z) ys = nubBy' ys zs
-- Append this element to the result and our internal list
| otherwise = z:nubBy' (z:ys) zs
allIntegers :: [(Nat, Nat)]
-- Remove duplicates under integer equality
allIntegers = nubBy intEqual allPairs
allIntegersMap :: Nat -> (Nat, Nat)
allIntegersMap n = allIntegers !! n
allRationals :: [(Nat, Nat)]
-- Add 1 to the numerator and denominator to get rid of 0,
-- then remove duplicates under fraction equality
allRationals = nubBy ratEqual $ map (\(a,b) -> (a+1, b+1)) allPairs
allRationalsMap :: Nat -> (Nat, Nat)
allRationalsMap n = allRationals !! n
```
For completeness's sake, the resulting pairs of each map are as follows
| *n* | `allIntegersMap n` | `allRationalsMap n` |
|-----|--------------------|---------------------|
| 0 | (0, 0) = 0 | (1, 1) = 1 |
| 1 | (0, 1) = -1 | (1, 2) = 1/2 |
| 2 | (1, 0) = 1 | (2, 1) = 2/1 |
| 3 | (0, 2) = -2 | (1, 3) = 1/3 |
| 4 | (2, 0) = 2 | (3, 1) = 3/1 |
| 5 | (0, 3) = -3 | (1, 4) = 1/4 |
| 6 | (3, 0) = 3 | (2, 3) = 2/3 |
| 7 | (0, 4) = -4 | (3, 2) = 3/2 |
| 8 | (4, 0) = 4 | (4, 1) = 4/1 |
| 9 | (0, 5) = -5 | (1, 5) = 1/5 |
| ... | ... | ... |
Note that the tuples produced by `allIntegers`, when interpreted as integers, happen to coincide with the earlier enumeration given by `listIntegers`.
Tree of Fractions
-----------------
There's an entirely separate structure which contains all rationals in least terms. It relies on an operation between two fractions called the *mediant*. For two rational numbers in least terms *p* and *q*, such that *p* < *q*, the mediant is designated *p* ⊕ *q* and will:
1. also be in least terms (with some exceptions, see below),
2. be larger than *p*, and
3. be smaller than *q*
$$
p = {a \over b} < {c \over d} = q, \quad \gcd(a,b) = \gcd(c,d) = 1 \\ ~ \\
p < p \oplus q < q \quad \phantom{\gcd(a+c, b+d) = 1} \\ ~ \\
{a \over b} < {a+c \over b+d} < {c \over d}, \quad \gcd(a+c, b+d) = 1
$$
We know our sequence of rationals starts with 1/1, 1/2, and 2/1. If we start as before with 1/1 and want to get the other quantities, then we can take its mediants with 0/1 and 1/0, respectively (handwaving the fact that the latter isn't a legitimate fraction).
$$
\begin{align*}
&& && \large{1 \over 1} && && \\
{ \oplus {0 \over 1} } && \large{/} && && \large{\backslash} ~ && \oplus {1 \over 0} \\
&& \large{1 \over 2} && && \large{2 \over 1} &&
\end{align*}
$$
We might try continuing this pattern by doing the same thing to 1/2. We can take its mediant with 0/1 to get 1/3. Unfortunately, the mediant of 1/2 and 1/0 is 2/2 (as is the mediant of 2/1 with 0/1), which isn't in least terms, and has already appeared as 1/1.
We could try another fraction that's appeared in the tree. Unfortunately, 2/1 suffers from the same issue as 1/0 -- 1/2 ⊕ 2/1 = 3/3, which is the same quantity as before, despite both fractions being in least terms. On the other hand, 1/2 ⊕ 1/1 = 2/3, which is in least terms. Similarly, 2/1 ⊕ 1/1 is 3/2, its reciprocal.
$$
\begin{align*}
&& && \large{1 \over 2} && && \\
{ \oplus {0 \over 1} } && \large{/} && && \large{\backslash} ~ && \oplus {1 \over 1} \\
&& \large{1 \over 3} && && \large{2 \over 3} &&
\end{align*}
\qquad \qquad
\begin{align*}
&& && \large{2 \over 1} && && \\
{ \oplus {1 \over 1} } && \large{/} && && \large{\backslash} ~ && \oplus {1 \over 0} \\
&& \large{3 \over 2} && && \large{3 \over 1} &&
\end{align*}
$$
The trick is to notice that a step to the left "updates" what the next step to the right looks like. Steps to the right behave symmetrically. For example, in the row we just generated, the left child of 2/3 is its mediant with 1/2, its right child is its mediant with 1/1.
Continuing this iteration ad infinitum forms the so-called [Stern-Brocot tree](https://en.wikipedia.org/wiki/Stern%E2%80%93Brocot_tree). A notable feature of this is that it is a [binary search tree](https://en.wikipedia.org/wiki/Binary_search_tree) (of infinite height). This means that for any node, the value at the node is greater than all values in the left subtree are and less than all values in the right subtree.
![]()
There's a bit of a lie in presenting the tree like this. As a binary tree, it's most convenient to show the nodes spaced evenly, but the distance between 1/1 and 2/1 is not the same as the distance between 1/1 and 1/2.
We can implement this in Haskell using `Data.Tree`. This package actually lets you describe trees with any number of child nodes, but we only need two for the sake of the Stern-Brocot tree.
```{.haskell}
import Data.Tree
-- Make a tree by applying the function `make` to each node
-- Start with the root value (1, 1), along with
-- its left and right steps, (0, 1) and (1, 0)
sternBrocot = unfoldTree make $ ((1,1), (0,1), (1,0)) where
-- Place the first value in the tree, then describe the next
-- values for `make` in a list:
make (v@(vn, vd), l@(ln, ld), r@(rn, rd))
= (v, [
-- the left value, and its left (unchanged) and right steps...
(((ln + vn), (ld + vd)), l, v),
-- and the right value, and its left and right (unchanged) steps
(((vn + rn), (vd + rd)), v, r)
])
```
### Cutting the Tree Down
We're halfway there. All that remains is to read off every value in the tree as a sequence. Perhaps the most naive way would be to read off by always following the left or right child. Unfortunately, these give some fairly dull sequences.
```{.haskell}
treePath :: [Int] -> Tree a -> [a]
treePath xs (Node y ys)
-- If we don't have any directions (xs), or the node
-- has no children (ys), then there's nowhere to go
| null xs || null ys = [y]
-- Otherwise, go down subtree "x", then recurse with that tree
-- and the rest of the directions (xs)
| otherwise = y:treePath (tail xs) (ys !! head xs)
-- Always go left (child 0)
alwaysLeft = treePath (repeat 0) sternBrocot
-- = [(1,1),(1,2),(1,3),(1,4),(1,5),(1,6),(1,7),(1,8),(1,9),(1,10),...]
-- i.e., numbers with numerator 1
-- Always go right (child 1)
alwaysRight = treePath (repeat 1) sternBrocot
-- = [(1,1),(2,1),(3,1),(4,1),(5,1),(6,1),(7,1),(8,1),(9,1),(10,1),...]
-- i.e., numbers with denominator 1
```
Rather than by following paths in the tree, we can instead do a breadth-first search. In other words, we read off each row individually, in order. This gives us our sequence of rational numbers with no repeats.
$$
\begin{align*}
\mathbb{N^+}& ~\rightarrow~ \mathbb{Q} \\
n & ~\mapsto~ \text{bfs}[n]
\end{align*}
\\ ~ \\
1 \mapsto 1/1,~ \\
2 \mapsto 1/2,\quad 3 \mapsto 2/1,~ \\
4 \mapsto 1/3,\quad 5 \mapsto 2/3, \quad 6 \mapsto 3/2, \quad 7 \mapsto 3/1,~ ...
$$
For convenience, this enumeration is given starting from 1 rather than from 0. This numbering makes it clearer that each row starts with a power of 2, since the structure is a binary tree, and the complexity doubles with each row. The enumeration could just as easily start from 0 by starting with $\N$, then getting to $\N^+$ with $n \mapsto n+1$.
We can also write a breadth-first search in Haskell, for posterity:
```{.haskell}
bfs :: Tree a -> [a]
bfs (Node root children) = bfs' root children where
-- Place the current node in the list
bfs' v [] = [v]
-- Pluck one node off our list of trees, then recurse with
-- the rest, along with that node's children
bfs' v ((Node y ys):xs) = v:bfs' y (xs ++ ys)
sternBrocotRationals = bfs sternBrocot
```
The entries in this enumeration have already been given.
### Another Tree
Another tree of fractions to consider is the tree of binary fractions. These fractions simply consist of odd numbers divided by powers of two. The most convenient way to organize these into a tree is to keep denominators equal if the nodes have the same depth from the root. We also stipulate that we arrange the nodes as a binary search tree, like the Stern-Brocot tree.
The tree starts from 1/1 as before. Its children have denominator 2, so we have 1/2 to the left and 3/2 to the right. This is equivalent to subtracting 1/2 for the left step and adding 1/2 for the right step. At the next layer, we want fractions with denominator 1/4, and do similarly. In terms of adding and subtracting, we just use 1/4 instead of 1/2.
![]()
We can describe this easily in Haskell:
```{.haskell}
-- Start with 1/1 (i.e., (1, 1))
binFracTree = unfoldTree make $ (1,1) where
-- Place the first value in the tree, then describe the next
-- values for `make` in a list:
make v@(vn, vd)
= (v, [
-- double the numerator and denominator, then subtract 1 from the numerator
(2*vn - 1, 2*vd),
-- same, but add 1 to the numerator instead
(2*vn + 1, 2*vd)
])
```
The entries of this tree have an additional interpretation when converted to their binary expansions. These fractions always terminate in a "1" in binary, but ignoring this final entry, starting from the root and following "left" for 0 and "right" for 1 places us at that fraction in the tree. In other words, the binary expansions encode the path from the root to the node.
Why Bother?
-----------
The tree of binary fractions and the Stern-Brocot tree are both infinite binary search trees, so we might imagine overlaying one tree over the other, pairing up the individual entries.
![]()
In Haskell, we can pair up entries recursively:
```{.haskell}
zipTree :: Tree a -> Tree b -> Tree (a,b)
-- Pair the values in the nodes together, then recurse with the child trees
zipTree (Node x xs) (Node y ys) = Node (x,y) $ zipWith zipTree xs ys
binarySBTree = zipTree sternBrocot binFracTree
```
Conveniently, both left subtrees of the root fall in the interval (0, 1). It also pairs up 1 and 1/2 with themselves. Doing so establishes a bijection between the rationals and the binary rationals in that interval. Rationals are more continuous than integers, so it might be of some curiosity to plot this function. We only have to look at a square over the unit interval. Doing so reveals a curious shape:
:::: {}
::: {}
![]()
:::
::: {}
![]()
:::
Left: binary rationals on the x-axis, rationals on the y-axis. <br>
Right: rationals on the x-axis, binary rationals on the y-axis.
::::
The plot on the right which maps the rationals to the binary rationals is known as [Minkowski's question mark function](https://en.wikipedia.org/wiki/Minkowski%27s_question-mark_function). Notice that this function is nearly 1/2 for values near 1/2 (nearly 1/4 for values near 1/3, nearly 1/8 for values near 1/4, etc.).
### I'm Repeating Myself
The inverse question mark map (which I'll call ¿ for short), besides mapping binary rationals to rationals, has an interesting relationship with other rational numbers. Recall that we only defined the function in terms of fractions which happen to have finite binary expansions. Those with infinite binary expansions, such as 1/3 (and indeed, any fraction whose denominator isn't a power of 2) aren't defined.
$$
{1 \over 2} = 0.1_2 \\
{1 \over 3} = 0.\overline{01} = 0.\textcolor{red}{01}\textcolor{green}{01}\textcolor{blue}{01}... \\
{1 \over 4} = 0.01_2 \\
{1 \over 5} = 0.\overline{0011} = 0.\textcolor{red}{0011}\textcolor{green}{0011}\textcolor{blue}{0011}... \\
\vdots
$$
We can persevere if we continue to interpret the binary strings as a path in the tree. This means that for 1/3, we go left initially, then alternate between going left and right. As we do so, let's take note of the values we pass along the way:
```{.haskell}
-- Follow the path described by the expansion of 1/3
treePath (0:cycle [0,1]) $ zipTree sternBrocot binFracTree
```
| | Binary fraction | Binary fraction (decimal) | Stern-Brocot rational | Stern-Brocot rational (decimal)
|-----|-------------------|---------------------------|-----------------------|--------------------------------|
| 0 | 1/1 | 1 | 1/1 | 1 |
| 1 | 1/2 | 0.5 | 1/2 | 1/2 |
| 2 | 1/4 | 0.25 | 1/3 | 0.3333333... |
| 3 | 3/8 | 0.375 | 2/5 | 0.4 |
| 4 | 5/16 | 0.3125 | 3/8 | 0.375 |
| 5 | 11/32 | 0.34375 | 5/13 | 0.38461538... |
| 6 | 21/64 | 0.328125 | 8/21 | 0.32812538... |
| 7 | 43/128 | 0.3359375 | 13/34 | 0.3823529... |
| 8 | 85/256 | 0.33203125 | 21/55 | 0.38181818... |
| ⋮ | ⋮ | ⋮ | ⋮ | ⋮ |
| 100 | (too big to show) | 0.3333333333... | (too big to show) | 0.381966011... |
| ⋮ | ⋮ | ⋮ | ⋮ | ⋮ |
:::: {layout-ncol = "2"}
::: {}
![]()
:::
::: {}
![]()
:::
Left: binary convergents of 1/3 <br>
Right: ¿ applied to binary convergents of 1/3, which also appear to converge
::::
Both sequences appear to converge to a number, with the binary fractions obviously converging to 1/3. The rationals from the Stern-Brocot don't appear to be converging to a repeating decimal. Looking closer, the numerators and denominators of the fractions appear to come from the Fibonacci numbers. In fact, the quantity that the fractions approach is $2 - \varphi$, where φ is the golden ratio. This number is the root of the polynomial $x^2 - 3x + 1$.
In fact, all degree 2 polynomials have roots that are encoded by a repeating path in the Stern-Brocot tree. Put another way, ¿ can be extended to map rationals other than binary fractions to quadratic roots (and ? maps quadratic roots to rational numbers). This is easier to understand when writing the quantity as its [continued fraction expansion](https://en.wikipedia.org/wiki/Continued_fraction), but that's an entirely separate discussion.
Either way, it tells us something interesting: not only can all rational numbers be enumerated, but so can quadratic *irrationals*.
### The Other Side
I'd like to briefly digress from talking about enumerations and mention the right subtree. The question mark function, as defined here, is only defined on numbers between 0 and 1 (and even then, technically only rational numbers). According to Wikipedia's definition, the question mark function is quasi-periodic -- $?(x + 1) = ?(x) + 1$. On the other hand, according to the definition by pairing up the two trees, rationals greater than 1 get mapped to binary fractions between 1 and 2.
:::: {layout-ncol = "2"}
::: {}
![]()
:::
::: {}
![]()
:::
Question mark function including right subtree. <br>
Left: linear x-axis. <br>
Right: (base 2)-logarithmic x-axis.
::::
Here are graphs describing *our* question mark function, on linear and logarithmic plots. Instead of repeating, the function continues its self-similar behavior as it proceeds onward to infinity (logarithmically). The right graph stretches from -∞, where its value would be 0, to ∞, where its value would be 2.
Personally, I like this definition a bit better, if only because it matches other ways of thinking about the interval (0, 1). For example,
- In topology, it's common to show that this interval is homeomorphic to the entire real line
- It's similar to the [rational functions which appear in stereography](), which continue to infinity instead of being periodic
- It showcases how the Stern-Brocot tree sorts rational numbers by complexity better
However, it's also true that different definitions are good for different things. For example, periodicity matches the intuition that numbers can be decomposed into a fractional and integral part. Integral parts grow without bound, while fractional parts are periodic, just like the function would be.
Closing
-------
I'd like to draw this discussion of enumerating numbers to a close for now. I wrote this article to establish some preliminaries regarding *another* post that I have planned. On the other hand, since I was describing the Stern-Brocot tree, I felt it also pertinent to show the question mark function, since it's a very interesting self-similar curve. Even then, I have shown them as a curiosity instead of giving them their time in the spotlight.
I have omitted some things I would like to have discussed, such as [order type](https://en.wikipedia.org/wiki/Order_type), and enumerating things beyond just the quadratic irrationals. I may return to some of these topics in the future, such as to show a way to order integer polynomials.
Diagrams created with GeoGebra (because trying to render them in LaTeX would have taken too long) and Matplotlib (yes, I called into a Python interpreter from Haskell out of laziness).

View File

@ -0,0 +1,508 @@
---
format:
html:
html-math-method: katex
---
<style>
.red {
color: red;
}
.green {
color: green;
}
</style>
Numbering Numbers, Part 2: Ordering Obliquely
=============================================
This post assumes you have read the [previous one](), which discusses the creation of sequences containing every integer, every binary fraction, and in fact, every fraction. From the fractions, we also could enumerate quadratic irrational numbers using the *question-mark function*.
Other Irrationals
-----------------
Because rationals -- and even some irrationals -- can be enumerated, we might imagine that it would be nice to enumerate *all* irrational numbers. Unfortunately, we're not very lucky this time.
Let's start by making what we mean by "number" more direct. We've already been exposed to infinite expansions, like 1/3 in binary (and conveniently, also decimal). In discussing the question mark function, I mentioned the utility of repeating expansions as a means of accessing quadratic rationals in the Stern-Brocot tree. A general sequence need not repeat, so we choose to treat these extra "infinite expansions" as numbers. Proving that a rational number must have a repeating expansion is difficult, but if we accept this premise, then the new non-repeating expansions are our irrationals. For example, $2 - \varphi \approx 0.381966...$, which we encountered last time, is such a number.
Doing this introduces a number of headaches, the least of which is attempting to do arithmetic with such quantities. However, we're only concerned with the contents of these sequences to show why we can't list out all irrationals.
### Diagonalization
We can narrow our focus to the interval between 0 and 1, since the numbers outside this interval are their reciprocals. Now (if we agree to use base ten), "all numbers between 0 and 1" as we've defined them begin with "0.", followed by an infinite sequence of digits 0-9. Suppose that we have an enumeration of every infinite sequence on this list -- no sequence is left out. [Cantor's diagonal argument](https://en.wikipedia.org/wiki/Cantor%27s_diagonal_argument) shows that a new sequence can be found by taking the sequence on the diagonal and changing each individual digit to another one. The new sequence differs in at least one place from every element on the list, so it cannot be on the list, showing that such an enumeration cannot exist.
This is illustrated for binary sequences in this diagram:
::: {}
<figure class="wp-block-image size-full">
<a title="Jochen Burghardt, CC BY-SA 3.0 <https://creativecommons.org/licenses/by-sa/3.0>, via Wikimedia Commons" href="https://commons.wikimedia.org/wiki/File:Diagonal_argument_01_svg.svg"><img width="256" alt="Diagonal argument 01 svg" style="background-color:white !important; border-radius: 5px" src="https://upload.wikimedia.org/wikipedia/commons/thumb/b/b7/Diagonal_argument_01_svg.svg/256px-Diagonal_argument_01_svg.svg.png"></a>
<figcaption>
Source: Jochen Burghardt, via Wikimedia Commons
</figcaption>
</figure>
:::
- It's fairly common to show this argument without much further elaboration, but there are a few problems with it:
- We're using infinite sequences of digits, not numbers
- Equality between sequences is defined by having all elements coincide
- We assume we have an enumeration of sequences to which the argument applies. The contents of the enumeration are a mystery.
- We have no idea where the rational numbers are, or if we'd construct one by applying the diagonal argument
### Equality
The purpose of the diagonal argument is to produce a new sequence which was not previously enumerated. The sequence is different in all positions, but what we actually want is equality with respect to the base. In base ten, we have the peculiar identity that [$0.\overline{9} = 1$](https://en.wikipedia.org/wiki/0.999...). This means that if the diagonal argument (applied to base ten sequences) constructs a new sequence with the digit 9 repeating forever, it might be equivalent to a sequence which was already in the list:
```{python}
#| echo: false
from IPython.display import Markdown
from tabulate import tabulate
red = lambda x: f"<span class=\"red\">{x}</span>"
green = lambda x: f"<span class=\"green\">{x}</span>"
def zipconcat(*args):
return [sum(z, start=[]) for z in zip(*map(list, args))]
diag = lambda f, yss: [
[
f(y) if i == j else y
for j, y in enumerate(ys)
]
for i, ys in enumerate(yss)
]
Markdown(tabulate(
zipconcat(
map(lambda x: [x], [0, 1, 2, green(3), 4, 5, "...", green("ω")]),
diag(red, [
[1, 2, 3, 4, 5, 6],
[9, 2, 4, 8, 3, 7],
[2, 2, 8, 2, 2, 2],
map(green, [2, 3, 9, 3, 9, 9]),
[9, 4, 9, 4, 9, 4],
[8, 1, 2, 5, 7, 9],
["...", "...", "...", "...", "...", "..."],
map(green, [2, 3, 9, 4, 0, 0]),
]),
[["..."]]*8,
),
headers=["Sequence", *range(6), "..."],
))
```
A case in which the diagonal argument could construct a number already on the list.
In the above table, sequence 3 is assumed to continue with 9's forever. The new sequence ω comes from taking the red digits along the diagonal and mapping each digit to the next one (mod ten). In the enumeration given, the diagonal continues with 9's forever, so we end up with 0's forever in ω.
### Picking a Sequence and Ensuring Rationals
"No problem, just pick another enumeration," you might say. Indeed, the example given relies on a simple function applied on the diagonal and an enumeration to which it is particularly ill-suited.
Instead, let's focus on something we *can* do. Instead of assuming we have all irrational numbers listed out already, let's start smaller. As of last post, we already have several ways to enumerate all rational numbers between 0 and 1. If we take this enumeration, convert rational numbers to their expansions in a base, and apply the diagonal argument, the resulting quantity *should* be an irrational number.
For convenience, we'll use the binary expansions of rationals as our sequences. That way, to get a unique sequence on the diagonal, we only have to worry about changing "0"s to "1"s (and vice versa). Since we have less flexibility in our digits, it also relieves us from some of responsibility of finding a "good" function, like in the decimal case. However, it's still possible the argument constructs a number already equal to something on the list.
Converting to Silicon
---------------------
Before going any further, let's convert the diagonal argument into a function. We want to, given an enumeration of binary sequences, produce a new sequence not on the list. This can be implemented in Haskell fairly easily:
```{.haskell}
diagonalize :: [[Int]] -> [Int]
-- pair each sequence with its index
diagonalize xs = let sequences = zip [0..] xs in
-- read out the red diagonal
let diagonal = map (\(n, s_n) -> (s_n !! n)) sequences in
-- map 0 to 1 and vice versa
map (1 -) diagonal
-- or in point-free form
-- diagonalize = map (\(x,y) -> 1 - (y !! x)) . zip [0..]
```
Nothing about this function is specific to "binary sequences", since `Int` contains values other than `1` and `0`. It's just more intuitive to work with them instead of "`True`" and "`False`" (since `Bool` actually does have 2 values). You can replace `1 -` with `not` to get a similar function for the type `Bool`.
We also need a function to get a rational number's binary expansion. This is simple if you recall how to do long division. We try to divide the numerator by the denominator, "emit" the quotient as part of the result list, then continue with the remainder (and the denominator is unchanged). It's not *quite* that simple, since we also need to "bring down" more zeroes. In binary, we can add more zeroes to an expansion by just multiplying by 2.
```{.haskell}
binDiv :: Int -> Int -> [Int]
-- Divide n by d; q becomes part of the result list
-- The rest of the list comes from doubling the remainder and keeping d fixed
binDiv n d = let (q,r) = n `divMod` d in q:binDiv (2*r) d
x = take 10 $ binDiv 1 3
-- x = [0,0,1,0,1,0,1,0,1,0]
-- 1/3 = 0.01010101...
y = take 10 $ binDiv 1 5
-- y = [0,0,0,1,1,0,0,1,1,0]
-- 1/5 = 0.00110011...
```
This function gives us the leading 0 (actually the integer part of the quotient of n by d), but we can peel it off by applying tail.
Since we intend to interpret these sequences as binary numbers, we might as well also convert this into a form we recognize as a number. All we need to do is take a weighted sum of each sequence by its binary place value.
```{.haskell}
fromBinSeq :: Int -> [Int] -> Double
-- Construct a list of place values starting with 1/2
fromBinSeq p = let placeValues = (tail $ iterate (/2) 1) in
-- Convert the sequence to a type that can multiply with
-- the place values, then weight, truncate, and sum.
sum . take p . zipWith (*) placeValues . map fromIntegral
oneFifth = fromBinSeq 64 $ tail $ binDiv 1 5
-- oneFifth = 0.2 :: Double
```
The precision *p* here is mostly useless, since we intend to take this to as far as doubles will go. *p* = 100 will do for most sequences, since it's rare that we'll encounter more than a few zeroes at the beginning of a sequence.
Some Enumerations
-----------------
Now, for the sake of argument, let's look at an enumeration that fails. Using the tree of binary fractions from the last post, we use a breadth-first search to create a list of terminating binary expansions.
::: {}
![]()
Tree of binary fractions, as the tree of terminating binary expansions. We perform a BFS on the left subtree to obtain the values between 0 and 1.
:::
```{python}
#| echo: false
Markdown(tabulate(
zipconcat(
[
[0, "1/2"],
[1, "1/4"],
[2, "3/4"],
[3, "1/8"],
[4, "3/8"],
[5, "5/8"],
[6, "7/8"],
[7, "1/16"],
["...", "..."],
["ω", ""],
],
diag(red, [
[1, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0],
[1, 1, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0],
[0, 1, 1, 0, 0, 0],
[1, 0, 1, 0, 0, 0],
[1, 1, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 0],
["...", "...", "...", "...", "...", "..."],
[0, 0, 1, 1, 1, 1],
]),
[["..."]]*10,
),
headers=["*n*", "Number", *range(6), "..."],
))
```
```{.haskell}
-- Extract the left subtree (i.e., the first child subtree)
badDiag = let (Node _ (tree:__)) = binFracTree in
diagonalize $ map (tail . uncurry binDiv) $ bfs tree
```
Computing the diagonal sequence, it quickly becomes apparent that we're going to keep getting "0"s along the diagonal. This is because we're effectively just counting in binary some number of digits to the right. The length of a binary expansion grows logarithmically rather than linearly; in other words, we can't count fast enough by just adding 1 (or adding any number, really).
Even worse than this, we get a sequence which is equal to sequence 1 as a binary expansion. We can't even rely on the diagonal argument to give us a new number that *isn't* equal to a binary fraction.
### Stern-Brocot Diagonal
The Stern-Brocot tree contains more than just binary fractions, so we're bound to encounter more than "0" forever when running along the diagonal. Again, looking at the left subtree, we can read off fractions between 0 and 1.
![]()
We end up with the following enumeration:
```{python}
#| echo: false
Markdown(tabulate(
zipconcat(
[
[0, "1/2"],
[1, "1/3"],
[2, "2/3"],
[3, "1/4"],
[4, "2/5"],
[5, "3/5"],
[6, "3/4"],
[7, "1/5"],
["...", "..."],
["ω", ""],
],
diag(red, [
[1, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 1, 0, 1, 0, 1],
[1, 0, 1, 0, 1, 0, 1, 0],
[0, 0, 1, 0, 0, 0, 0, 0],
[0, 1, 1, 0, 0, 1, 1, 0],
[1, 0, 0, 1, 1, 0, 0, 1],
[1, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 0, 0, 1, 1],
["...", "...", "...", "...", "...", "...", "...", "..."],
[0, 0, 0, 1, 1, 1, 1, 0],
]),
[["..."]]*10,
),
headers=["*n*", "Number", *range(8), "..."],
))
```
```{.haskell}
-- Extract the left subtree (i.e., the first child subtree)
sbDiag = let (Node _ (tree:__)) = sternBrocot in
diagonalize $ map (tail . uncurry binDiv) $ bfs tree
-- take 20 sbDiag = [0,0,0,1,1,1,1,0,1,1,0,1,1,1,1,1,0,0,1,1]
-- in other words, 0.00011110110111110011...
```
When expressed as a decimal, the new sequence corresponds to the value 0.12059395276... . Dually, its continued fraction expansion begins \[0; 8,3,2,2,1,2,12, ...\]. While the number is (almost) certainly irrational, I have no idea whether it is algebraic or transcendental.
### Pairs, without repeats
We have a second, similar enumeration given by `allRationalsMap` in the previous post. We'll need to filter out the numbers greater than 1 from this sequence, but that's not too difficult since we're already filtering out repeats.
```{python}
#| echo: false
Markdown(tabulate(
zipconcat(
[
[0, "1/2"],
[1, "1/3"],
[2, "1/4"],
[3, "2/3"],
[4, "1/5"],
[5, "1/6"],
[6, "2/5"],
[7, "3/4"],
["...", "..."],
["ω", ""],
],
diag(red, [
[1, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 1, 0, 1, 0, 1],
[0, 1, 0, 0, 0, 0, 0, 0],
[1, 0, 1, 0, 1, 0, 1, 0],
[0, 0, 1, 1, 0, 0, 1, 1],
[0, 0, 1, 0, 1, 0, 1, 0],
[0, 1, 1, 0, 0, 1, 1, 0],
[1, 1, 0, 0, 0, 0, 0, 0],
["...", "...", "...", "...", "...", "...", "...", "..."],
[0, 0, 1, 1, 1, 1, 0, 1],
]),
[["..."]]*10,
),
headers=["*n*", "Number", *range(8), "..."],
))
```
```{.haskell}
-- Only focus on the rationals whose denominator is bigger
arDiag = let rationals01 = filter (uncurry (<)) allRationals in
diagonalize $ map (tail . uncurry binDiv) $ rationals01
-- take 20 arDiag = [0,0,1,1,1,1,0,1,0,1,1,1,0,1,0,0,0,1,0,0]
-- in other words, 0.00111101011101000100...
```
This new sequence has a decimal expansion equivalent to 0.24005574958... (continued fraction expansion begins \[0; 4, 6, 28, 1, 1, 5, 1, ...\]). Again, this is probably irrational, since WolframAlpha has no idea on whether a closed form exists.
The Diagonal Transform
----------------------
Why stop with just one? This new number can just be tacked onto the beginning of the list. Then, we re-apply the diagonal argument to obtain a new number. And so on ad infinitum.
```{python}
#| echo: false
diag2 = lambda d, yss: [
[
d.get(i - j, lambda x: x)(y)
for j, y in enumerate(ys)
]
for i, ys in enumerate(yss)
]
Markdown(tabulate(
[["...", "", *(["..."]*9)]]
+ zipconcat(
[
[green("-2"), ""],
[red("-1"), ""],
[0, "1/2"],
[1, "1/3"],
[2, "2/3"],
[3, "1/4"],
[4, "2/5"],
[5, "3/5"],
[6, "3/4"],
[7, "1/5"],
["...", "..."],
],
diag2(
{
1: green,
2: red,
},
[
[1, 1, 1, 1, 1, 0, 1, 1],
[0, 0, 0, 1, 1, 1, 1, 0],
[1, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 1, 0, 1, 0, 1],
[1, 0, 1, 0, 1, 0, 1, 0],
[0, 0, 1, 0, 0, 0, 0, 0],
[0, 1, 1, 0, 0, 1, 1, 0],
[1, 0, 0, 1, 1, 0, 0, 1],
[1, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 0, 0, 1, 1],
["...", "...", "...", "...", "...", "...", "...", "..."],
]
),
[["..."]]*12,
),
headers=["*n*", "Number", *range(8), "..."],
))
```
Using the Stern-Brocot enumeration ~~because I like it better~~
```{.haskell}
transform :: [[Int]] -> [[Int]]
-- Emit the new diagonal sequence, then recurse with the new sequence
-- prepended to the original enumeration
transform xs = let ds = diagonalize xs in ds:transform (ds:xs)
sbDiagSeq = let (Node _ (tree:__)) = sternBrocot in
transform $ map (tail . uncurry binDiv) $ bfs tree
-- take 10 $ map (fromBinSeq 100) $ sbDiagSeq
-- = [0.12059395276295479, 0.9839683315741587, 0.39401828609177836,
-- 0.7474914867558182, 3.876798422930419e-2, 0.7802097209903278,
-- 0.3215249242021624, 0.6795283379777878, 0.1938245109955674,
-- 0.947203605609322]
```
Does this list out "all" irrational numbers? Not even close. In fact, this just gives us a bijection between the original enumeration and a new one containing our irrationals. The new numbers we get depend heavily on the order of the original sequence. This is obvious just by looking at the first entry produced by our two good enumerations. Perhaps if we permuted the enumeration of rationals in all possible ways, we would end up listing all irrational numbers, but we'd also run through [all ways of ordering the natural numbers](https://en.wikipedia.org/wiki/Baire_space_%28set_theory%29).
The fact that "bad" enumerations exist tells us that it's not even guaranteed that we don't collide with any rationals. I conjecture that the good enumerations won't do so, since we shouldn't ever encounter an infinite sequence of "0"s, and a sequence should eventually differ in at least one position from one already listed.
### Almost Involution
Since the function has the same input and output type, you may wonder what happens when it's applied multiple times. Perhaps you assume that, since `\x -> 1 - x` swaps 0 and 1, we'll get the original sequence back again. Alas, experimentation proves us wrong.
```{.haskell}
sbDiagSeq2 = transform sbDiagSeq
-- take 10 $ map (fromBinSeq 100) $ sbDiagSeq2
-- [0.5, 1/2
-- 0.3333333333333333, 1/3
-- 0.9166666666666666, 11/12
-- 0.125, 1/8
-- 0.7124999999999999, 57/80
-- 0.2875, 23/80
-- 0.859375, 55/64
-- 7.5e-2, 3/40
-- 0.6841517857142857, ???, possibly 613/896
-- 0.486328125] 249/512
```
The fraction forms of the above numbers are my best guesses. Either way, it only matches for the first two terms before going off the rails.
Even stranger than this distorted reflection is the fact that going through the mirror again doesn't take us anywhere new
```{.haskell}
sbDiagSeq3 = transform sbDiagSeq2
-- take 10 $ map (fromBinSeq 100) $ sbDiagSeq3
-- = [0.12059395276295479, 0.9839683315741587, 0.39401828609177836,
-- 0.7474914867558182, 3.876798422930419e-2, 0.7802097209903278,
-- 0.3215249242021624, 0.6795283379777878, 0.1938245109955674,
-- 0.947203605609322]
--
-- (take 10 $ map (fromBinSeq 100) $ sbDiagSeq3) \
-- == (take 10 $ map (fromBinSeq 100) $ sbDiagSeq)
-- = True
```
In other words, applying the transform thrice is the same as applying the transform once. Not that this has been shown rigorously in the least -- it's only been tested numerically for the first 10 numbers that we generate.
Regardless, this semi-involutory behavior is strange and nontrivial. And it's not limited to this enumeration.
```{.haskell}
arDiagSeq = let rationals01 = filter (uncurry (<)) allRationals in
transform $ map (tail . uncurry binDiv) rationals01
-- -- What goes in isn't what comes out
-- take 10 $ map (fromBinSeq 100) $ transform arDiagSeq
-- [0.5, 1/2
-- 0.3333333333333333, 1/3
-- 0.75, 3/4
-- 0.16666666666666666, 1/6
-- 0.7, 7/10
-- 0.41666666666666663, 5/12
-- 0.8687499999999999, 139/160
-- 0.1015625, 13/128
-- 0.5295758928571428, ???, possibly 949/1792
-- 0.49453125] 633/1280
--
--
-- -- And applying the transform twice is sometimes an identity (maybe)
-- (take 10 $ map (fromBinSeq 100) $ transform $ transform arDiagSeq) \
-- == (take 10 $ map (fromBinSeq 100) $ arDiagSeq)
-- = True
bfDiagSeq = let (Node _ (tree:__)) = binFracTree in
transform $ map (tail . uncurry binDiv) $ bfs tree
-- -- The bad enumeration just gives more binary fractions...
-- take 10 $ map (fromBinSeq 100) bfDiagSeq
-- [0.25, 1/4
-- 1.0, 1/1
-- 0.5, 1/2
-- 0.625, 5/8
-- 6.25e-2, 1/16
-- 0.78125, 25/32
-- 0.390625, 25/64
-- 0.6328125, 81/128
-- 0.19140625, 49/256
-- 0.814453125] 417/512
-- -- But it still doesn't mean double-transforming gives the original
-- take 10 $ map (fromBinSeq 100) $ transform bfDiagSeq
--
-- [0.5, 1/2
-- 0.25, 1/4
-- 0.75, 3/4
-- 0.125, 1/8
-- 0.6875, 11/16
-- 0.46875, 15/32
-- 0.859375, 55/64
-- 0.1171875, 15/128
-- 0.55859375, 143/256
-- 0.435546875] 223/512
```
What does it mean that the initial enumeration comes back as a completely different one? Since the new one is "stable" with respect to applying the diagonal transform twice, are the results somehow preferable or significant?
Closing
-------
These questions, and others I have about the products of this process, I unfortunately have no answer for. I was mostly troubled by how rare it is to find people applying the diagonal argument to something to anything oh random sequences. Random sequences, either by choice or by algorithm (if you can even call it random at that point), are really only good to show how to *apply* the diagonal argument. Without knowing *what* the argument is applied to, it's useless as a tool. It also still remains to prove that your new sequences are sufficient for the purpose (and I've only shown a case in which it fails).
(Recycled) diagrams made in Geogebra. Downloadable Haskell code, including things from the previous post available [here]().