When learning the material on decidability/computability, it’s helpful to be armed with a toolbox of different strategies for gluing algorithms together, or translating between kinds of algorithms or inputs and outputs.
If you have one algorithm that computes function f
, and another algorithm that computes function g
, it’s straightforward to specify an algorithm that computes their composition g∘f
. You just feed your input into the f
algorithm, and whatever it gives as output, you then feed into your g
algorithm.
We can do something more general than that. Suppose we have a function g
that expects to take not one but three inputs. And we have three functions f₁
, f₂
, and f₃
. Each of these takes two inputs. Then we can specify a kind of generalized composition g∘(f₁,f₂,f₃)
. This algorithm would take two inputs, feed copies of them to each of f₁
, f₂
, and f₃
, and feed each of their outputs as inputs into g
. The same could clearly be done for any adicity of our g
and f
functions.
Another way of gluing algorithms together that we discussed in class is, suppose we have two algorithms f₁
and f₂
. We might know in advance that they’ll give the same result, or that only one of them will ever deliver a result. The other will keep going forever. Then we can specify an algorithm where we give it descriptions of the f₁
algorithm, and the f₂
algorithm, and what input we want them to work on. Let’s call this the “parent algorithm.” What our parent algorithm would then do is trace out a finite number of steps of the f₁
algorithm when fed the desired input. If it hasn’t yet delivered a result, then the parent algorithm pauses on f₁
, and instead traces out a finite number of steps of the f₂
algorithm, giving it the same input. Then back to f₁
. Whichever of these two algorithms delivers a result first, the parent algorithm will then stop and give that as its result. Gluing algorithms together in this way is known as nondeterministic choice. The word “nondeterminism” in these contexts doesn’t mean that probabilities or randomness is involved. The parent algorithm doesn’t toss a coin and then use one of the algorithms and ignore the other. Instead, both algorithms get used, each a bit at a time, until one of them delivers a result. (If the algorithms are both capable of delivering results for a given input, and they would give different results, then we haven’t specified how the parent algorithm behaves.)
In discussions of Turing Machines, a parent algorithm that’s capable of taking as input a description of some other Turing Machine, and then tracing through its execution to see what the other Turing Machine would do, is called a Universal Turing Machine. There can be many of these because they can respond differently to the results of what the other Turing Machine would do; also some of them might trace through just a single other Turing Machine’s excecution, or some as in our previous example might trace through the execution of multiple Turing Machines. Early on in the development of physical computers, they had to build a new machine (or at least change the wiring) for each new problem they wanted to solve. Building a physical implementation of a Universal Turing Machine is like when they transitioned to building physical computers where you could feed them a program — that is a description of what algorithm you wanted them to execute — and have them operate on that. The computers we have nowadays are almost always of this sort. (They only really approximate Universal Turing Machines, though, because our physical computers have limited amounts of memory, whereas the algorithms we’re talking about in this course don’t have any such limits.)
We could even specify a parent algorithm that traces out the execution of countably infinite other Turing machines, so long as we could devise a finite way of specifying those machines to feed our parent algorithm as input. What the parent algorithm might do is trace one execution step on the first of its child algorithms, then one execution step on the second and then again one on the first, then one execution step on the third then one again on the second and one again on the first, and so on. Eventually, it will have traced out any number of steps on any of its child algorithms, unless and until one of them delivers a result, which the parent algorithm will then return as its result.
IN PROGRESS! WHEN THIS SECTION IS COMPLETE I’LL REMOVE THIS NOTICE
Sometimes one has a problem where it’s most natural to think of the input (or the output, or both) as being strings of some language, but then one is working with a formal model where the inputs (or the output, or both) are expected to be numbers (elements of ℕ). Or it might go the other way: it’s natural to think of the input and/or output as numbers, but you’re working with a formal model where they’re expected to be strings.
A related issue is when the formal model expects to receive just one number/string as input, and return just one as output, but it’s most natural to think of the input and/or output as being a collection of numbers/strings. (Here we’re just thinking about when you want a collection of outputs given all at once; we’ll talk later about algorithms that deliver a sequence a bit at a time.)
We already considered this, back in Homework 1 Problems 12–14.
Generally we are assuming our strings are built from a finite alphabet (set of letters). Some of what we do with strings would also work if the alphabet was countably infinite (but not larger than that); but this makes things more complicated, so we’ll assume only finite alphabets.
If the alphabet has only a single letter, then the strings built from it correspond straightforwardly to numbers: empty
pairs with 0
, "a"
pairs with 1
, "aa"
pairs with 2
, "aaa"
pairs with 3
, and so on. An abbreviation theorists often use is to write "a"n to mean n
concatenated copies of "a"
. So "a"
would be "a"1, "aa"
would be "a"2, and so on. Then we can state the corrspondence to numbers very concisely: "a"n pairs with n
.
If the alphabet has more than one letter — let’s say it has m
letters — then one strategy to use would be to pair off each letter with a number between 0
and m - 1
, and then let the string consitute a base m
numeral. For example, if we have two letters "a"
and "b"
then "bbab"
might be interpreted as a binary numeral expression of 1⋅eight + 1⋅four + 0⋅two + 1⋅one
, which is thirteen. The only difficulty here is that whatever letter you pair with 0
, if it comes at the start of the string it’s not going to make a difference to the result. Thus in the encoding just suggested, "abbab"
would also represent thirteen, even though it’s a different string. For some purposes, this might not matter; for others it will. One workaround would be to instead interpret "a"
and "b"
as the non-zero digits in a base 3 numeral; and not have any letter paired with the digit 0. The downside of that is that then some numbers won’t have any strings associated with them. Again, for some purposes, this might not matter; for others it will.
Your email client may use a variation of this strategy called Base64 to encode attachments. In that scheme, each of the upper- and lower-case Latin letters is treated as a digit, and so are 0
..9
, and so are two punctuation characters (usually +
and /
). Giving 64 digits altogether. An email attachment, interpreted as a sequence of numbers, is translated into a base-64 string so that it can be safely conveyed through the email networks, which often reject data that isn’t normal textual characters.
A different strategy for pairing strings with numbers, when the alphabet has more than one letter, is to order the strings somewhat alphabetically, and then pair each string with its position in that order. I say “somewhat alphabetically” because we have to choose the order carefully. If we used the “lexicographic ordering” from Homework 4 Problem 6, as we saw that ordering is not well-founded. So it would pair empty
with 0
, and then "a"
with 1
, and "aa"
with 2
, "aaa"
with 3
, and so on. It would never get around to pairing the strings "aab"
, "ab"
, "b"
, and so on, with any numbers. We’d have to use a different ordering. A convenient one is the ordering I’ve sometimes used in class, where first we go through all the length 0 strings (empty
), then all the length 1 strings in alphabetic order ("a"
then "b"
) then all the length 2 strings in alphabetic order ("aa"
then "ab"
then "ba"
then "bb"
) and so on. This is called a shortlex ordering. So long as we’ve decided which order the letters come in, it determines a unique pairing between every string and every number. We could use it to go from strings to numbers, or to go from numbers to strings.
There are multiple ways to do this. One idea is based on a strategy for a “linear walk of an infinite 2D table” that we considered when discussing cardinality:
This would pair (0,0)
with position 0
, (0,1)
with position 1
, (1,0)
with position 2
, (0,2)
with position 3
, (1,1)
with position 4
, and so on.
For reference, here are the functions for going from the pair to their single-number encoding by this strategy, and then back again:
methodAFromPair(x,y) =def (x + y)⋅(x + y + 1)/2 + x
methodAToPair(n) =def let m = (sqrt(1 + 8⋅n) - 1)/2 in
let x = n - m⋅(m + 1)/2 in
let y = m - x in
(x,y)
In the second function, sqrt(z)
is a function that returns the greatest element of ℕ
whose square is ≤ z
: thus sqrt(7)
is 2
. Also, the divisions by 2
throw away any remainder; this matters for the first line of methodAToPair
.
A different strategy is to pair (0,0)
with position 0
, then skip one and pair (0,1)
with position 2
, skip one again and pair (0,2)
with position 4
, and so on. Then we pair (1,0)
with the first position left unpaired with (0,anything), namely position 1
. We skip one unpaired position (position 3
) and pair (1,1)
with the next, being position 5
, and so on. Then we pair (2,0)
with the first position left unpaired with (0,anything) or (1,anything), namely position 3
. We skip one unpaired position and pair (2,1)
with the next, and so on.
For reference, here are the functions for going from the pair to their single-number encoding by this strategy, and then back again:
methodBFromPair(x,y) =def 2x⋅(2⋅y + 1) - 1
methodBToPair(n) =def let x = cto(n) in let y = (n + 1)/2x + 1 in (x,y)
In the second function, cto(n)
is a function that returns how many consecutive 1
s there are at the right-side of the binary representation of n
. Since nineteen in binary is 10011
, cto(19) = 2
. As before, the division in methodBToPair
throws away any remainder.
One of the readings I gave you is this selection from Boolos, Burgess, and Jeffrey on countable and uncountable sets. Our methodAFromPair
is akin to their function J
in Chapter 1, our methodAToPair
akin to their function G
, our methodBFromPair
akin to their function j
, and our methodBToPair
akin to their function g
. The difference is that at this point in their text, these authors are working with numbers starting from 1
rather than ones starting from 0
. (Later they switch to numbers starting from 0
, as we’re doing throughout.)
One expedient strategy for encoding (x, y, z)
is to treat it like (x, (y, z))
, and then use one of the methods discussed earlier for encoding pairs (twice). Or you could treat it like ((x, y), z)
and do the same. Or you could come up with a custom strategy for dealing with triples instead of pairs.
What if you have a finite sequence of numbers that you want to translate to a single number, but you don’t know in advance how long the sequence will be? Let’s say you have the length n
sequence [x₀, x₁, ... xn-1], assuming n ≥ 1
. Then you might treat that like the pair (n, xx)
, where xx
is the encoding for pairs, or triples, or … whatever kind of length n
sequence you have. If you have a length 1
sequence [x₀]
— then xx
will just be that single number x₀
. Then the pair (n, xx)
can itself be encoded using a strategy for encoding pairs.
Boolos, Burgess, and Jeffrey use this strategy at the start of their Example 1.9.
If you wanted to also allow for empty (length-0) sequences, you’d have to modify this somewhat. (What should you use for xx
in that case?)
A different strategy would be to let xx
be 2x₀⋅3x₁⋅...⋅pxn-1, where p
is the n
th prime. We’d still then need to encode (n, xx)
, in order to tell the difference between, for example, the sequence [1,2]
and the sequence [1,2,0]
.
Boolos, Burgess, and Jeffrey use something like this strategy at the end of their Example 1.9; but they don’t need to worry about elements of the sequence being 0
.
Most of these strategies would leave us with some numbers that don’t have any sequences paired with them. Again, for some purposes this might not matter; for others it will.
A different strategy would be to encode your sequence of numbers as a single string (see next entry), and then translate that back into a single number.
If you have a sequence of numbers xi, you can encode each of them as a string, and then use the method mentioned above (from Homework 1 Problems 12–14) for encoding a sequence of strings as a single string.
A different strategy for pairing (a restricted selection of) strings with pairs of numbers is suggested in Homework 5 Problem 5. This can be generalized to longer sequences of numbers, so long as you have enough letters to work with. (Alternatively, you could alternate between just two letters, but in that case you’d need to make sure that none of the numbers being encoded is 0
.)
Of course, these various translations become tedious, but you don’t have to do them by hand. Just specify an algorithm that’s capable of doing them for you!