Nixon Diamond

Two rules of thumb disagree about one person: which one wins, and why.

nixon-diamond.py · output · proof · check · try it in the playground


The question

A default is a rule of thumb: it holds unless something says otherwise. In this fictional rulebook, Quakers are pacifists by default, and Republicans are nonpacifists. The rulebook also says which rule wins when they clash: the Republican one.

Nixon is both a Quaker and a Republican. Ada is only a Quaker. (The names and memberships are made up for the example; they say nothing about anyone’s real beliefs.)

Drawn as a picture, Nixon sits at the bottom of a diamond with one path up to pacifist and one to nonpacifist: hence the name.

Is Nixon a pacifist? Is Ada?


What we tell eyekind

fact(quaker('nixon'))
fact(republican('nixon'))
fact(quaker('ada'))

implies([quaker(Person), not_(nonpacifist(Person))], pacifist(Person))
implies(republican(Person), nonpacifist(Person))

implies(quaker(Person), classification(Person, 'quaker'))
implies(republican(Person), classification(Person, 'republican'))
implies(pacifist(Person), classification(Person, 'pacifist'))
implies(nonpacifist(Person), classification(Person, 'nonpacifist'))

query(classification(Person, Class))

The first three lines are facts: things we simply state as true.

implies(condition, conclusion) is a rule: whenever the condition holds, eyekind concludes the conclusion. A list in square brackets means all of these. Person and Class are variables: capitalized placeholders that can stand for anyone.

The first rule is the default, with a guard: not_(nonpacifist(Person)) reads “nothing concludes that this person is a nonpacifist”. So a Quaker is a pacifist unless the nonpacifist rule says otherwise. That guard is what gives the Republican rule priority.

The four classification rules collect every label a person receives, and query(...) asks eyekind to report all of them.


The default waits its turn

A rule that says “nothing concludes X” must not be used too early: if it ran before the nonpacifist rule, it would wrongly find nothing.

eyekind therefore sorts the rules into layers, called strata. The rules that can conclude nonpacifist form an earlier layer; the default, which looks for the absence of nonpacifist, runs only after that layer has finished. This is stratified negation: “not found” is only asked once all the contrary evidence is in.


What eyekind concludes

// Answers: each instance of a query() that holds.
classification('nixon', 'quaker')
classification('ada', 'quaker')
classification('nixon', 'republican')
classification('nixon', 'nonpacifist')
classification('ada', 'pacifist')

The comment line says what follows: the filled-in versions of the question that are true.


The proof, line by line

eyekind --proof writes down how it reached each answer. This is the complete proof:

// Answers: each instance of a query() that holds.
classification('nixon', 'quaker')
classification('ada', 'quaker')
classification('nixon', 'republican')
classification('nixon', 'nonpacifist')
classification('ada', 'pacifist')

// Clauses: the program clauses the steps cite, by clause number.
clause(1, fact(quaker('nixon')))
clause(2, fact(republican('nixon')))
clause(3, fact(quaker('ada')))
clause(4, implies([quaker(Person), not(nonpacifist(Person))], pacifist(Person)))
clause(5, implies(republican(Person), nonpacifist(Person)))
clause(6, implies(quaker(Person), classification(Person, 'quaker')))
clause(7, implies(republican(Person), classification(Person, 'republican')))
clause(8, implies(pacifist(Person), classification(Person, 'pacifist')))
clause(9, implies(nonpacifist(Person), classification(Person, 'nonpacifist')))

// 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(classification('nixon', 'quaker'), clause(6), {Person: 'nixon'}, [quaker('nixon')])
step(quaker('nixon'), clause(1), {}, [])
step(classification('ada', 'quaker'), clause(6), {Person: 'ada'}, [quaker('ada')])
step(quaker('ada'), clause(3), {}, [])
step(classification('nixon', 'republican'), clause(7), {Person: 'nixon'}, [republican('nixon')])
step(republican('nixon'), clause(2), {}, [])
step(classification('nixon', 'nonpacifist'), clause(9), {Person: 'nixon'}, [nonpacifist('nixon')])
step(nonpacifist('nixon'), clause(5), {Person: 'nixon'}, [republican('nixon')])
step(classification('ada', 'pacifist'), clause(8), {Person: 'ada'}, [pacifist('ada')])
step(pacifist('ada'), clause(4), {Person: 'ada'}, [quaker('ada'), not(nonpacifist('ada'))])
step(not(nonpacifist('ada')), 'absent', {}, [])

It has three parts:

Two kinds of justification occur:

Read the steps as sentences:

  1. Nixon is classified as a Quaker — by rule 6, with Person = nixon, because Nixon is a Quaker.
  2. Nixon is a Quaker — fact 1.
  3. Ada is classified as a Quaker — by rule 6, with Person = ada, because Ada is a Quaker.
  4. Ada is a Quaker — fact 3.
  5. Nixon is classified as a Republican — by rule 7, because Nixon is a Republican.
  6. Nixon is a Republican — fact 2.
  7. Nixon is classified as a nonpacifist — by rule 9, because Nixon is a nonpacifist.
  8. Nixon is a nonpacifist — by rule 5, because Nixon is a Republican (step 6).
  9. Ada is classified as a pacifist — by rule 8, because Ada is a pacifist.
  10. Ada is a pacifist — by the default, rule 4, because Ada is a Quaker (step 4) and nothing says she is a nonpacifist.
  11. Nothing says Ada is a nonpacifist — absent: eyekind looked, after the nonpacifist rule had finished, and found nothing.

There is no step for “Nixon is a pacifist”: the default could not fire for him, because step 8 makes him a nonpacifist.


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:

// Answers checked subject to the obligations below.
classification('nixon', 'quaker')
classification('ada', 'quaker')
classification('nixon', 'republican')
classification('nixon', 'nonpacifist')
classification('ada', 'pacifist')

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

// Obligations: absence and collection completeness are taken on trust.
obligation('absent', 'theory_scoped', not(nonpacifist('ada')))

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

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

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

The obligation line spells out what was taken on trust: an 'absent' step, that nonpacifist('ada') cannot be concluded. 'theory_scoped' means the absence is relative to this program: with these facts and rules, nothing makes Ada a nonpacifist. A different rulebook could.

The statistics repeat this: 11 steps, 10 verified against the program, 1 trusted. The verdict is checked with obligations: every answer follows from the program, provided that the absence really holds. As the last comment says, the verdict does not establish completeness: it does not say these are the only classifications there could be.


Try it

eyekind examples/nixon-diamond.py
eyekind --check-proof examples/proof/nixon-diamond.py examples/nixon-diamond.py
eyekind --proof examples/nixon-diamond.py
eyekind -c examples/nixon-diamond.py

Delete the line fact(republican('nixon')) and run again: nothing blocks the default any more, and Nixon is classified as a pacifist, like Ada. The check report now lists two absences, one for each of them.

Or remove the guard: replace the default with implies(quaker(Person), pacifist(Person)). Nixon is then classified as both a pacifist and a nonpacifist. eyekind does not object to the clash, because nothing in the program says the two cannot go together; and with no absence left, the verdict becomes plain checked.


Takeaway

When defaults conflict, decide which one wins and write it down. A guarded default makes the priority explicit, and the proof shows exactly which “nothing found” each conclusion leans on.