← Contents · Ch 0 · Ch 1

Vertical Subtraction

Described, implemented in Lean, visualised, and proved correct. repo

This chapter builds directly on Vertical Addition. Subtraction has the same column-by-column shape, the same three-layer structure, and a proof with the same backbone. Rather than repeat all of it, we lean on addition throughout and concentrate on the one thing subtraction adds. If carries, the layered design, or the induction there are not yet comfortable, read that chapter first.

1 · What the algorithm does

Subtraction has the same column-by-column shape as addition, but in place of a carry it uses a borrow. You line the two numbers up by their right edge and subtract column by column from the units. When the top digit in a column is too small, ten is taken from the column to its left before subtracting, and that column is then short by one. The borrow is the only thing that passes between columns. It's the carry's mirror image.

Addition had nothing like this next part. Subtraction is defined only when the top number is at least the bottom one; otherwise the true result is negative, which a list of digits cannot represent. That is a real precondition, and the whole proof turns on it.

Take 452 − 27. Working from the units, when a column can't subtract you take ten from the column to its left: that column's digit is crossed out and rewritten one smaller, and the borrowing column gains +10.

Why it's built this way. Two modelling choices, both to stay faithful to the paper method. We subtract column by column with a borrow — the mirror of addition's carry — rather than leaning on some built-in operation. And hand subtraction only makes sense when the top is at least the bottom; below that there is no schoolbook answer. So the algorithm carries that as an explicit precondition (returning an empty list as a sentinel if it is ever broken), instead of silently truncating to zero the way Lean's Nat subtraction does. Keeping the precondition honest is what lets the theorem mean what we want — see the prologue's note.

2 · The implementation, in Lean

Subtraction is built in the same three layers as addition, each proved correct before the next is placed on top, with a borrow wherever addition had a carry.

The second case — top exhausted, bottom still has digits — is reached either when the precondition is broken, or when the bottom's remaining digits are all zeros (think 5 − 05), in which case [] (value 0) is exactly the right answer. The theorem's hypothesis keeps the branch honest: it forces the remaining bottom value to 0. Termination, exactly as for addition, follows from the combined list length decreasing at each call.

3 · See it run — the invariant

Step through a column at a time, the way you would borrow on paper: when a column can't subtract, it takes ten from its left neighbour — that neighbour's digit is crossed out and dropped by one, and the borrowing column is marked +10. On paper you edit the neighbour's digit; the code never edits — it hands the next column a borrow flag, and subtracting that 1 there is the same act. When the neighbour is 0 the strike chains: 0 becomes 9 and the borrow moves one more column left (try 100 − 1 below). The panel states subtraction's invariant; entering a top number smaller than the bottom brings the precondition into play.

After the lowest k columns, the top digits read so far, plus any borrow still owed at its place value 10ᵏ, equal the result digits plus the bottom digits read so far:

aₖ + borrow × 10ᵏ = resultₖ + bₖ.

This is the invariant, the borrow-shaped counterpart of addition's. Each column re-establishes it. At the end no digits and no borrow remain, and provided the borrow never escaped past the top number, it collapses to exactly a − b = result. That proviso is the precondition again.

One caution before §5: there the borrow will change sides. In the panel the borrow sits with a, because the columns already read have taken ten from the ones still to come; the theorem views the same debt from the other side — an incoming borrow the remaining columns must pay, so it joins output + b. Two views of one ledger.

Predict one column

One column of subDigits takes two digits and an incoming borrow, and returns a difference digit and a borrow-out. With a borrow coming in, what does subDigits 5 5 true return?

PREDICT

4 · Before the proof — think like a verifier

You have read one of these proofs already, back in the addition chapter, so this time see how much of it you can anticipate before reading it. A verification mindset is less about Lean syntax than about a few habits: pin down for which inputs the claim is even meant to hold, account for every branch the code can take (including the ones that look wrong), and check that each assumption you rely on still holds the next time around. The questions below are those habits, applied to subtraction.

What must the theorem assume?

Addition's theorem held for all inputs. Subtraction by hand can fail — try 3 − 7 with whole numbers. So what has to appear in verticalSub_correct that addition's statement never needed?

CHOOSE

The branch that looks wrong

The [], _ :: _ case returns [] — on its face a wrong difference. If the algorithm can reach that branch, how can the theorem still be true?

CHOOSE

What the recursion costs you

The proof is an induction: it assumes the algorithm is correct on the shorter tails, then handles one more column. But the inductive hypothesis only applies when its own precondition holds. What extra work does that force at each step?

CHOOSE

5 · The proof

Same shape as the algorithm, and told the same way as addition's proof. First the argument in plain words: read just these and you'll understand why the algorithm is correct. Then, folded underneath, the full Lean proof, for anyone who wants the machine-checked details — you can follow the whole section without opening a single dropdown.

The single-column facts are settled case by case; verticalSub_correct is then proved by induction. What sets it apart from addition is the point the questions above were circling: the precondition b ≤ a has to be re-earned before each recursive call — that is the whole job of the small hpre sub-proofs. Notice h is slightly stronger than §4's answer: the owed borrow joins the bottom side, deliberately, because each recursive call passes a new borrow inward, so the inequality the induction maintains must mention it. That is what keeps the borrow from ever asking a column to subtract more than it holds, and it is why the empty-list "undefined" branch can be discharged rather than feared. A short corollary, verticalSub_correct', then drops the borrow for the everyday no-borrow case.

In the Lean, goal states in the tooltips are lightly abbreviated — · for *, and b' names this column's borrow-out — the shapes are what matter.

6 · Did the proof land?

A few questions about the pieces that make the proof work, plus one design choice in the definition it leans on. These are no longer predictions. They ask whether the moving parts are clear now that you have read it.

Why keep the borrow general?

CHOOSE

What subDigits_correct does inside the induction

CHOOSE

Where does the proof of hpre get its ammunition?

CHOOSE

The theorem has no minus sign

The statement reads output + b + borrow = a. Why avoid stating output = a − b?

CHOOSE

The shape of the proof

The proof inducts on a, and again on b inside each case. Why this nested, two-list structure?

CHOOSE

The impossible-digit case in subTable

The table ends with | ⟨n+10, h⟩, _ => absurd h (by omega). Why is that line there at all?

CHOOSE

Back to addition: the both-empty base case

Addition's first base case was | [], [], true => [⟨1, …⟩], but subtraction's is | [], [], true => []. Both lists are empty with a leftover bit — why does addition produce a digit while subtraction produces nothing?

CHOOSE

7 · Write some Lean

A worksheet. Each cell has a starting goal with a gap for you to fill. Press Run and your code is compiled by Lean on the server. Run all checks every cell in turn, Reset all restores the blank cells, and every cell hides a reference solution you can reveal once you've tried. In the listings digit literals are written 0, 1 for readability; the server spells them ⟨0, by omega⟩ — either compiles. (Requires the checker server — see web/checker/README.md. The first run is slow: Lean is cold-loading Mathlib.)

The real, machine-checked proof is exactly the one in §5 — clone the repo and run it in VS Code.

← Vertical Addition Contents Long Multiplication →