This note is organised so that we will:
- Introduce and motivate structural induction.
- Go through some minor differences between structural and “regular” induction.
- Go through the tutorial questions as examples of how to use structural induction.
What is structural induction?
To see this, let’s take a step back and think about what regular induction effectively does:
You’re basically giving a proof that says we have a starting point (our base case), and from that base case we can prove more and more cases. And typically these are an additive offset away from our base case. Like or or anything along those lines. Typically these induction proofs aim to prove for all where is some base case number in .
Recursive Structure
Structural induction is a generalised form of induction where we free ourselves from:
- Dealing with numbers as our cases.
- Being confined to proving statements that must be true for all .
Without those constraints, it turns out we can prove a whole lot more statements. It also matches a very recursive viewpoint of a lot of the things that we know. To free ourselves up in this way, we need to define a recursive structure that we can induct on.
As a simple example, let’s say that we wanted to define set to be the set of valid mathematical expressions that involve adding numbers. So here are some examples of expressions that are valid:
- 123
- 123 + 412
- 312 + 5412 + 3110
Examples of expressions that are not valid are:
- +
- 1 +
- 1 ++ 2
- + 2 + 3
- 2 + 3 + 5 +
How would you define this? One way would be to declare set inductively in the following way:
- . (Base case)
- (Inductive case)
We’ve basically giving a recursive structure.
To be clear, the second clause is basically saying that if you can give me two valid expressions from , then the expression that adds the two of them are also in . E.g. we know that is a valid expression (base case). So is a valid expression (Inductive case). We also know that is a valid expression for similar reasons, so is a valid expression. So is something like .
Notice this set doesn’t contain all possible strings! Only the ones we care about.
Okay that was a bit of a simplistic example, you can imagine how we might want to handle something more complicated with though.
Note again, that this is a generalisation of the normal induction you’ve been looking at. Why? Because we can just recursively define the structure for all the natural numbers like so:
- . (Base clause)
- . (Recursion clause)
- Membership for can always be demonstrated by (finitely many) successive applications of clauses above. (Minimality clause)
But now, we’re freed up to also do many things like reason about algorithms on lists, or trees. For example:
Example
Define a non-empty list of T to be either:
- A pair of a T and null; Or
- A pair of T and a list of T.
Example
Define a tree of type T is either:
- A list of type T; or
- A list of trees of type T.
Now we can do something like finding the maximum value of a non-empty list or summing the values in a tree and prove that our algorithm is correct. For example here’s a program that outputs the maximum value of a non-empty list:
function max(xs) {
return is_null(tail(xs)) ? head(xs) : max2(head(xs), max(tail(xs)));
}
Similarly here’s a program that outputs the depth of a tree:
function depth(tree) {
const depths = map(
elem => !is_null(elem) && !is_pair(elem) ? 0 : depth(elem), tree);
return max(pair(0, depths)) + 1;
}
And now that we have that set up, here’s an example of how to prove your program is correct:
Claim: max(xs)
correctly computes the maximum value of a list of numbers.
- [Base Case]
xs
is a pair of a number andnull
.- Then
head(xs)
is the number, and the maximum value in the list is alsohead(xs)
. - Thus
max(xs)
returns the correct value in this case.
- Then
- [Inductive Case]
xs
is a pair of an number, and a listtail(xs)
.- Assume that
tail(xs)
is a list such thatmax(tail(xs))
correctly returns the maximum value oftail(xs)
. Let this maximum be called . - Also, let be the value of
head(xs)
. - Then consider the output value of
max2(head(xs), max(tail(xs)))
. Call it . - which happens to be the maximum value of the entire list.
- Assume that
- Therefore
max(xs)
correctly computes the maximum value for all lists.
Okay that was just an idea, we actually were not too formal about what was going on. But just in case, here’s another one proving the correctness of depth
.
Claim: depth(tree)
correctly computes the depth of any tree.
In case you don’t know what the depth of a tree is, here’s an illustration:
We will say trees of a single element have depth 1, and trees that have subtrees have depth that is maximum of their sub-trees + 1.
- [Base Case]
xs
is a list of T.- Then since T is not a pair,
map(elem => !is_null(elem) && !is_pair(elem) ? 0 : depth(elem), tree);
evaluates to a list of 0 or an empty list. - Then
max(pair(0, depths))
evaluates to where is the maximum value of the list if it is non-empty. Which happens to be . Thus the program returns , which is correct.
- Then since T is not a pair,
- [Inductive Case]
xs
is a list of tree of T.- Then
map(elem => !is_null(elem) && !is_pair(elem) ? 0 : depth(elem), tree);
evaluates to a list of return values ofdepth
on each tree. - Assume that for all trees
elem
inxs
,depth(elem)
correctly computes the value of the tree represented byelem
. - Since we return
1 + max(pair(0, depths))
, this by definition is 1 plus the maximum of the depth of our sub-trees, which is the correct value.
- Then
- Therefore
depth(tree)
correctly computes the depth of any tree.
Important
These examples were perhaps a little straightforward but if anything, you shouldn’t take this as a sign that it means we don’t need to prove our programs are correct. If anything, it’s rather the case that it’s possible to write programs that are very easy to prove are correct.
In fact, one amazing aspect about functional programming (and without side effects) is precisely the case that doing things recursively on recursive structures allows you to very easily reason about the correctness of your program without having to write a formal proof because the code already mirrors the proof.
Tutorial question as an example
Hamming Numbers
Consider the following set:
Instead of defining it this way, consider this following (equivalent) definition:
- If , then , and , and .
- For any , membership in can always be demonstrated by finitely many applications of the above clauses.
So with the alternative definition, we want to claim that if you took any in the set (that was recursively defined), it has a unique representation in the form of .
Bear in mind here, that I’ve told you the two definitions are equivalent without proving it. So in some sense you can also think of this as partly proving that the two definitions are indeed equivalent (actually our proof will do a little more than that).
So, as before, we’ve given a recursive structure. So we should induct on the structure of .
- [Base Case] Consider .
- .
- Therefore, exists such that . (Existential Generalisation)
- Furthermore, assume exists such that . Then since must be otherwise, .
- Therefore is the unique sequence of values for which .
- [Inductive Case] Let , arbitrarily chosen. Further assume that has unique values such that .
- Consider . Then . Thus exists values such that .
- Furthermore, assume exists such that . Then . And .
- Assume for the sake of contradiction that at either: , , , then it must be that where at least one of is non-zero.
- But then that is a contradiction since are all prime so it must be that .
- Therefore and thus the values are unique.
- The arguments for , and work similarly. (This is where you’d use WLOG)
- Thus, for all , exists unique such that .
Showing all sorts of properties on recursively defined sets
Consider the following set :
- If then and .
- For any , membership in can always be demonstrated by finitely many applications of the above clauses.
It’s probably doable to show that are in . Why? Because:
- due to the base clause.
- because and thus .
- because and thus .
- because and thus .
- because and thus .
But how do we show and aren’t in ? We will want to show some properties about . To do this, since was defined recursively, we need to also use structural induction to prove that these properties indeed hold about all elements of .
E.g. we want to argue that .
- [Base Case] Consider .
- Since and is even, .
- [Inductive Case] Let , arbitrarily chosen. Further assume that is even, and .
- Now consider . Since is even, , . Thus . Thus , where .
- Also, .
- Thus, is even, and .
- Now consider . Since is even, , . Thus . Thus , where .
- Also, .
- Thus, is even, and .
- Thus for all , is even, and .
Upon proving this, we know that and cannot be in because if they were, then they would have been even and greater than or equals to .
Showing all sorts of properties on recursively defined sets (example 2)
- If then , , ,
- For any , membership in can always be demonstrated by finitely many applications of the above clauses.
So again, something like can be shown to be in . Because , and .
But what about ?
What we want to argue is that for all sets , elements must either both be in , or both not be in . I.e. .
- [Base Cases]
- We know it’s true for both and .
- [Inductive Case]
- Let , assume that , and .
- Consider . Now either (1) at least one of or has both of and or (2) both of them do not have and . (It’s only these two cases because of our assumption)
- If at least one of or has both and , then has both or . I.e. .
- If both of them do not have both and , then has neither nor . I.e. .
- Consider . Now either (1) both of and has both of and or (2) at most one of has both and . (It’s only these two cases because of our assumption)
- If both of and has both of and , then has both and . Therefore .
- If at most or has both of and , then does not have neither nor . Therefore .
- Consider . (This gets a little case bashy).
- Case 1: has both
- Then does not have either nor .
- Case 2: has both , but does not either nor .
- Then has both and .
- Case 3: Either does not have or has .
- Then does not have either nor .
- Therefore in all cases
- Case 1: has both
- Thus in all cases, for all .
With that, we can argue that is not in because it contains but not .