jlm-blog
~jlm

4-Feb-2024

Ouroboros in Std.Logic

Filed under: math — jlm @ 18:14

This post dissects a very short yet fascinating and enigmatic proof that can be found in the Logic module of the standard library of the Lean proof verifier:

theorem iff_not_self : ¬(a ↔ ¬a)
  | H => let f h := H.1 h h
         f (H.2 f)

OK, the fact being proved isn’t enigmatic — it’s stating that a proposition’s truth value is never the same as its own negation’s — but the proof thereof sure is. If that looks to you like some incomprehensible formal logic circling to eat its own tail, that’s because it kinda is.

However, it’s written extremely densely, which inhibits comprehension — which is sad, because the way it works is very interesting. It’s quite the rabbit hole, though, so let’s take our trusty spade and start digging. The very first thing we encounter is that Lean implicitly converts the ¬(a ↔ ¬a) into (a ↔ ¬a) → False. This is because Lean uses dependent type theory to represent logical statements and their proofs, so we need to discuss how these kinds of theories are used with formal logic.

Lean isn’t alone is doing this. Type theories are a common way to implement formal logic in proof assistants/verifiers, by having the types represent mathematical statements and instances of a type represent a proof of that type. An advantage of this technique is that a proof of P∧Q is trivial to generate from proofs of P and Q by modelling it with a type constructor as a product type, and similarly, a proof of P∨Q modeled as a sum type is trivial to construct from a proof of either P or Q. Also, everyone who’s programmed has some intuition regarding type theory even if they’ve never studied it.

There are disadvantages as well, though. It’s very easy to get confused between the various abstract ensembles of types, the specific types you’re working with, and the instances of these types you’re trying to construct. Problems spanning across these levels usually have solutions that are very short (slightly tweaking a statement to trigger pattern matching, or finding just the right arguments to a type constructor) but difficult to find. And while the proof assistants can often provide hinting within any one level, they generally can’t provide any guidance in spanning across them.

The other big disadvantage is they don’t handle negation as well as they handle the rest of the logical constructs. This is because there’s no relationship between a proof of P and of ¬P since at most one will exist, and having no relationship in logic means there’s nothing for a type theory to model. The typical way to handle this is to introduce False as either an instance of type Bool or as a type which by definition has no instances, and make it subject to special rules like False→P holds for every proposition P and there are no tautologies Q for which Q→False holds. Once False is defined, ¬P gets defined to be P→False.

And that’s why we’re now looking at

theorem iff_not_self : (a ↔ ¬a) → False
  | H => let f h := H.1 h h
         f (H.2 f)

The next question is what is that | doing there? I can’t find this in Lean’s documentation anywhere, which seems to always show theorems being proven by assigning them a proof with :=. (Lean’s syntax is undocumented and I’m not the only one bothered by that.) Experimentation indicates this is a function builder syntax, which we can write with typical lambda notation by

theorem iff_not_self : (a ↔ ¬a)→False :=
  λ H => let f h := H.1 h h
         f (H.2 f)

What’s this? Our theorem is a function? In this case, yes, because of how implication is handled. The way it works is that P→Q is modeled as function mapping proofs of P into proofs of Q. Here, we have a function that maps proofs of a ↔ ¬a to proofs of False. Does this mean we get to see a proof of False? Only if we can provide the function with a proof of a ↔ ¬a — but those don’t exist. Constructing a function that maps P to False is constructing a refutation of P, which serves as a proof of ¬P.

It might be clearer to add a type annotation to H:

theorem iff_not_self : (a ↔ ¬a)→False :=
  λ H:(a ↔ ¬a) => 
      let f h := H.1 h h
      f (H.2 f)

Or maybe it just adds clutter. That probably depends on how familiar you are with Lean (or closely related systems like Coq). For this article, I’ll keep them in the proofs, while retaining awareness that Lean can infer them automatically.

The next thing we dig up are the mysterious fields of H, H.1 and H.2. It directly follows from P↔Q that P→Q and Q→P, so for convenience Lean makes those available as the fields 1 and 2 of “Iff types”. We can give those names, and include the unnecessary but clarifying type annotations:

theorem iff_not_self : (a ↔ ¬a)→False :=
  λ H:(a ↔ ¬a) =>
      have ana : a → ¬a := H.1
      have naa : ¬a → a := H.2
      let f h := ana h h
      f (naa f)

Up until now, our digging has been tedious de-sugaring of Lean syntax and annotating of directly inferable types. But now we hit paydirt and actually get to the fun bit: doing logic! First, it’ll be helpful to convert the ¬a into a→False and give a name to naa f:

theorem iff_not_self : (a ↔ ¬a)→False :=
  λ H:(a ↔ ¬a) =>
      have ana : a → (a→False) := H.1
      have naa : (a→False) → a := H.2
      let f h := ana h h
      let naaf := naa f
      f naaf

This reveals what’s going on with that f function. ana is a function which takes two proofs of a and returns a proof of False. So what f does is takes a proof of a (called h) and calls ana with it twice, returning the resulting proof of False, giving f the type a→False. We can annotate that:

theorem iff_not_self : (a ↔ ¬a)→False :=
  λ H:(a ↔ ¬a) =>
      have ana : a → (a→False) := H.1
      have naa : (a→False) → a := H.2
      have f : a→False := λ h => ana h h
      let naaf := naa f
      f naaf

What does naa do? It takes a function that refutes a and returns a proof of a. And we just saw that f is a function which refutes a! So naaf := (naa f) is a proof of a.

Finally, since f is a function that maps proofs of a to False, f naaf is a proof of False. Wait, earlier I said that there were no proofs of False, yet here one is. The thing is the proof was constructed from a proof of a ↔ ¬a (named H), so what we’ve really proven is that (a ↔ ¬a)→False, i.e., ¬(a ↔ ¬a), and QED.

So, here we see an extremely elegant proof that uses gemination (ana h h) to generate a refutation (f : a→False) that establishes an attestation (naaf := naa f) that generates a contradiction (f naaf). And all that elegance is hidden away because it’s written as ¬(a ↔ ¬a) | H => let f h := H.1 h h; f (H.2 f) from a misplaced value on extreme brevity.

No Comments »

No comments yet.

RSS feed for comments on this post. TrackBack URI

Leave a comment

Powered by WordPress