Peano

Counting with nothing but zero and “one more” — and getting 5! = 120 out of it.

peano.py · output · proof · check · try it in the playground


The question

Can you do arithmetic without digits? In 1889 Giuseppe Peano showed that all natural numbers can be built from just two things: zero, and the next number after (the successor). Addition, multiplication and factorial can then be defined by a few rules, with no calculator at all.


Numbers as nested boxes

Write the successor of N as s(N). Then:

Number Written as
0 'zero'
1 s('zero')
2 s(s('zero'))
3 s(s(s('zero')))

A number is simply how many s(…) wrappers surround 'zero'. No built-in arithmetic is used anywhere in this example.


What we tell eyekind

# add(A, B, C) means A + B = C: A + 0 = A, and A + (B + 1) = (A + B) + 1.

fact(add(A, 'zero', A))
implied_by(add(A, s(B), s(C)), add(A, B, C))
# multiply(A, B, C) means A * B = C: A * 0 = 0, and A * (B + 1) = A * B + A.
fact(multiply(_, 'zero', 'zero'))
implied_by(multiply(A, s(B), C), [multiply(A, B, D), add(A, D, C)])
# 0! = 1, and (N + 1)! = (N + 1) * N!.
fact(factorial('zero', s('zero')))
implied_by(factorial(s(N), F), [factorial(N, Before), multiply(s(N), Before, F)])
# Addition run backward enumerates every way of splitting a known sum.
query(add(A, B, s(s(s('zero')))))
# One derivation chains all three relations: (1*2)+3 = 5, then 5! = 120.
query(
  multiply(s('zero'), s(s('zero')), Product),
  add(Product, s(s(s('zero'))), Sum),
  factorial(Sum, Factorial),
)

What eyekind concludes

// Answers: each instance of a query() that holds.
add(s(s(s('zero'))), 'zero', s(s(s('zero'))))
add(s(s('zero')), s('zero'), s(s(s('zero'))))
add(s('zero'), s(s('zero')), s(s(s('zero'))))
add('zero', s(s(s('zero'))), s(s(s('zero'))))
and(multiply(s('zero'), s(s('zero')), s(s('zero'))), add(s(s('zero')), s(s(s('zero'))), s(s(s(s(s('zero')))))), factorial(s(s(s(s(s('zero'))))), s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s('zero'))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

The first four lines answer the first question by running addition backward: the sum is known, the parts are not. They are 3 + 0, 2 + 1, 1 + 2 and 0 + 3.

The second query has three goals, so its answer comes back as one and(…) of all three: 1 × 2 = 2, 2 + 3 = 5, and 5! is the long tail of s(s(s(…: 120 s wrappers around 'zero'.


The proof, line by line

eyekind --proof writes down how it reached each answer. The whole proof has 198 steps and is about 190 KB, because the factorial writes out numbers up to 120 in full; see the full proof. Here are its claims, its clauses, and the first 10 steps, which prove the first four answers completely:

// Answers: each instance of a query() that holds.
add(s(s(s('zero'))), 'zero', s(s(s('zero'))))
add(s(s('zero')), s('zero'), s(s(s('zero'))))
add(s('zero'), s(s('zero')), s(s(s('zero'))))
add('zero', s(s(s('zero'))), s(s(s('zero'))))
and(multiply(s('zero'), s(s('zero')), s(s('zero'))), add(s(s('zero')), s(s(s('zero'))), s(s(s(s(s('zero')))))), factorial(s(s(s(s(s('zero'))))), s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s('zero'))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

// Clauses: the program clauses the steps cite, by clause number.
clause(1, fact(add(A, 'zero', A)))
clause(2, implied_by(add(A, s(B), s(C)), add(A, B, C)))
clause(3, fact(multiply(_0, 'zero', 'zero')))
clause(4, implied_by(multiply(A, s(B), C), [multiply(A, B, D), add(A, D, C)]))
clause(5, fact(factorial('zero', s('zero'))))
clause(6, implied_by(factorial(s(N), F), [factorial(N, Before), multiply(s(N), Before, F)]))

// Steps: each goal, what justifies it (a clause, a builtin, a control, or an
// absence or collection taken on trust), the bindings and the goals it uses.
step(add(s(s(s('zero'))), 'zero', s(s(s('zero')))), clause(1), {A: s(s(s('zero')))}, [])
step(add(s(s('zero')), s('zero'), s(s(s('zero')))), clause(2), {A: s(s('zero')), B: 'zero', C: s(s('zero'))}, [add(s(s('zero')), 'zero', s(s('zero')))])
step(add(s(s('zero')), 'zero', s(s('zero'))), clause(1), {A: s(s('zero'))}, [])
step(add(s('zero'), s(s('zero')), s(s(s('zero')))), clause(2), {A: s('zero'), B: s('zero'), C: s(s('zero'))}, [add(s('zero'), s('zero'), s(s('zero')))])
step(add(s('zero'), s('zero'), s(s('zero'))), clause(2), {A: s('zero'), B: 'zero', C: s('zero')}, [add(s('zero'), 'zero', s('zero'))])
step(add(s('zero'), 'zero', s('zero')), clause(1), {A: s('zero')}, [])
step(add('zero', s(s(s('zero'))), s(s(s('zero')))), clause(2), {A: 'zero', B: s(s('zero')), C: s(s('zero'))}, [add('zero', s(s('zero')), s(s('zero')))])
step(add('zero', s(s('zero')), s(s('zero'))), clause(2), {A: 'zero', B: s('zero'), C: s('zero')}, [add('zero', s('zero'), s('zero'))])
step(add('zero', s('zero'), s('zero')), clause(2), {A: 'zero', B: 'zero', C: 'zero'}, [add('zero', 'zero', 'zero')])
step(add('zero', 'zero', 'zero'), clause(1), {A: 'zero'}, [])
// … 188 more steps, 198 in total, prove the fifth answer: 1 × 2 = 2,
// 2 + 3 = 5, and 5! = 120, made of multiplication steps that are in turn
// made of addition steps.

It has three parts:

Reading numbers as digits, the 10 steps say:

  1. 3 + 0 = 3 — fact 1, with A = 3.
  2. 2 + 1 = 3 — by rule 2, with A = 2, B = 0 and C = 2, because 2 + 0 = 2.
  3. 2 + 0 = 2 — fact 1.
  4. 1 + 2 = 3 — by rule 2, because 1 + 1 = 2.
  5. 1 + 1 = 2 — by rule 2, because 1 + 0 = 1.
  6. 1 + 0 = 1 — fact 1.
  7. 0 + 3 = 3 — by rule 2, because 0 + 2 = 2.
  8. 0 + 2 = 2 — by rule 2, because 0 + 1 = 1.
  9. 0 + 1 = 1 — by rule 2, because 0 + 0 = 0.
  10. 0 + 0 = 0 — fact 1.

Each use of rule 2 peels one s off the second number and the sum, until fact 1 ends the chain. The remaining 188 steps work the same way, with rules 4 and 6 and facts 3 and 5 for multiplication and factorial.


Checked: what the report says

A proof is only worth something if someone else can check it. eyekind -c generates the proof and checks it against the program; --check-proof checks a saved proof. The checker never reasons by itself: it only confirms each step against the program. This is the complete report:

// Checked answers: these follow from the input program.
add(s(s(s('zero'))), 'zero', s(s(s('zero'))))
add(s(s('zero')), s('zero'), s(s(s('zero'))))
add(s('zero'), s(s('zero')), s(s(s('zero'))))
add('zero', s(s(s('zero'))), s(s(s('zero'))))
and(multiply(s('zero'), s(s('zero')), s(s('zero'))), add(s(s('zero')), s(s(s('zero'))), s(s(s(s(s('zero')))))), factorial(s(s(s(s(s('zero'))))), s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s(s('zero'))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

// C1-C8: resolution, acyclicity, justification, coverage,
// re-decision, boundary consistency, relevance and witness integrity.
condition('C1', 'resolution', 'ok', 198)
condition('C2', 'well_founded', 'ok', 198)
condition('C3', 'justification', 'ok', 198)
condition('C4', 'coverage', 'ok', 231)
condition('C5', 're_decision', 'ok', 0)
condition('C6', 'boundary_consistency', 'ok', 0)
condition('C7', 'relevance', 'ok', 203)
condition('C8', 'witness_integrity', 'ok', 0)

// Proof statistics: recorded steps and how they were checked.
steps(198)
verified(198)
recomputed(0)
composed(0)
trusted(0)
claims(5)

// This verdict applies to the claims above; it does not establish completeness.
verdict('checked')

Each condition line names a check, says 'ok', and counts what it looked at:

The statistics: 198 steps, all 198 verified against the program, none taken on trust, 5 claims. The verdict is checked: all five answers follow from the program. As the last comment says, this confirms the claims; it does not by itself say that the four splits of 3 are the only ones.


Try it

eyekind examples/peano.py            # the answers
eyekind --proof examples/peano.py    # answers with their proof
eyekind --goal "add(A, B, s(s(s('zero'))))" examples/peano.py

--goal asks your own question instead of the program’s queries; the last command repeats the first question alone. Try --goal "multiply(s(s('zero')), s(s(s('zero'))), P)": 2 × 3 comes back as s(s(s(s(s(s('zero')))))), which is 6. Or open it in the playground.


Takeaway

Arithmetic can be pure reasoning: two ideas, a few rules, and every result traced back to “zero” and “one more”. The same rules even run backward, answering “what adds up to 3?”.