--- title: "Numbering Numbers, Part 2: Ordering Obliquely" description: | How do we count an infinitude of numbers? format: html: html-math-method: katex date: "2023-12-31" date-modified: "2025-07-26" categories: - algebra - question-mark function - haskell --- ```{haskell} --| echo: false :l Diagonal :l Previous import Data.Tree import Data.Profunctor import IHaskell.Display.Blaze import Text.Blaze.Colonnade import qualified Text.Blaze.Html4.Strict as Html import qualified Text.Blaze.Html4.Strict.Attributes as Attr import Diagonal hiding (displayDiagTable) import Previous redSpan = (Html.span Html.! Attr.style (Html.toValue "color: red")) . Html.string greenSpan = (Html.span Html.! Attr.style (Html.toValue "color: green")) . Html.string displayDiagTable ps n = ect $ rmap style diagBox' where diagBox' = diagBox n ect = encodeCellTable (Attr.class_ $ Html.toValue "") -- style the diagonal as red and rows numbered as `ps` as green style = either (stringCell . show) htmlCell . displayRows (redSpan . show) (map (, greenSpan . show) ps) ``` This post assumes you have read the [previous one](./1), 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:
It's fairly common to show this argument without much further elaboration, but there are a few problems with doing so: - 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: ```{haskell} --| code-fold: true --| classes: plain --| fig-cap: A case in which the diagonal argument could construct a number already on the list. diagData = rowsOmega [ [1, 2, 3, 4, 5, 6], [9, 2, 4, 8, 3, 7], [2, 2, 8, 2, 2, 2], [2, 3, 9, 3, 9, 9], [9, 4, 9, 4, 9, 4], [8, 1, 2, 5, 7, 9] ] [2, 3, 9, 4, 0, 0] displayDiagTable [Just Omega, Just $ RN 3] 5 diagData ``` 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 the 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. Into 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} --| layout-ncol: 2 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 y = take 10 $ binDiv 1 5 putStrLn $ "x = 1 / 3 = 0." ++ (tail x >>= show) ++ "..." putStrLn $ "y = 1 / 5 = 0." ++ (tail y >>= show) ++ "..." ``` 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 print oneFifth ``` 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.