The Liar’s paradox is often the first paradox someone dealing with logic, even in an informal setting, encounters. It is intuitively paradoxical: how can a sentence be both true, and false? This contradicts (ahem) the law of non-contradiction, that states that “no proposition is both true and false”, or, symbolically, . Appealing to symbols like that gives us warm fuzzy feelings, because, of course, the algebra doesn’t lie!
There’s a problem with that the appeal to symbols, though. And it’s nothing to do with non-contradiction: It’s to do with well-formedness. How do you accurately translate the “this sentence is false” sentence into a logical formula? We can try by giving it a name, say (for liar), and state that must represent some logical formula. Note that the equality symbol here is not a member of the logic we’re using to express , it’s a symbol of this discourse. It’s metalogical.
But what should fill in the dots? is the sentence we’re symbolising, so “this sentence” must mean . Saying “X is false” can be notated in a couple of equivalent ways, such as or . We’ll go with the latter: it’s a surprise tool that will help us later. Now we know how to fill in the dots: It’s .
Truth tables demonstrating the equivalence between and , if you are classically inclined.
But wait. If , then , and also , and so… forever. There is no finite, well-formed formula of first-order logic that represents the sentence “This sentence is false”, thus, assigning a truth value to it is meaningless: Saying “This sentence is false” is true is just as valid as saying that it’s false, both of those are as valid as saying “ is true”.
Wait some more, though: we’re not done. It’s known, by the Curry-Howard isomorphism, that logical systems correspond to type systems. Therefore, if we can find a type-system that assigns a meaning to our sentence , then there must exist a logical system that can express , and so, we can decide its truth!
Even better, we don’t need to analyse the truth of logically, we can do it type-theoretically: if we can build an inhabitant of , then it is true; If we can build an inhabitant of , then it’s false; And otherwise, I’m just not smart enough to do it.
So what is the smallest type system that lets us assign a meaning to ?
A system of equirecursive types: 1
We do not need a complex type system to express : a simple extension over the basic simply-typed lambda calculus will suffice. No fancy higher-ranked or dependent types here, sorry!
As a refresher, the simply-typed lambda calculus has only:
- A set of base types ,
- Function types ,
- For each base type , a set of base terms ,
- Variables ,
- Lambda abstractions , and
- Application .
Type assignment rules for the basic calculus.
First of all, we’ll need a type to represent the logical proposition . This type is empty: It has no type formers. Its elimination rule corresponds to the principle of explosion, and we write it . The inference rule:
We’re almost there. What we need now is a type former that serves as a solution for equations of the form . That’s right: we’re just inventing a solution to this class of equations—maths!
These are the equirecursive types, . The important part here is equi: these types are entirely indistinguishable from their unrollings. Formally, we extend the set of type formers with type variables and -types , where acts as a binder for .
Since we invented types as a solution for equations of the form , we have that , where means “substitute everywhere occurs in ”. The typing rules express this identity, saying that anywhere a term might have one as a type, the other works too:
Adding these rules, along with the one for eliminating , to the calculus nets us the system . With it, one can finally formulate a representation for our -sentence: it’s .
There exists a closed term of this type, namely , which means: The “this sentence is false”-sentence is true. We can check this fact ourselves, or, more likely, use a type checker that supports equirecursive types. For example, OCaml with the
-rectypes compiler option does.
We’ll first define the empty type
void and the type corresponding to :
type void = | ;; type l = ('a -> void) as 'a ;;
Now we can define our proof of , called
yesl, and check that it has the expected type:
let yesl: l = fun k -> k k ;;
However. This same function is also a proof that… . Check it out:
let notl (x : l) : void = x x ;;
I am Bertrand Russell
Bertrand Russell (anecdotally) once proved, starting from , that he was the Pope. I am also the Pope, as it turns out, since I have on hand a proof that and , in violation of non-contradiction; By transitivity, I am Bertrand Russell.
Alright, maybe I’m not Russell (drat). But I am, however, a trickster. I tricked you! You thought that this post was going to be about a self-referential sentence, but it was actually about typed programming language design (not very shocking, I know). It’s a demonstration of how recursive types (in any form) are logically inconsistent, and of how equirecursive types are wrong.
The logical inconsistency, we all deal with, on a daily basis. It comes with Turing completeness, and it annoys me to no end every single time I accidentally do
let x = ... x .... I really wish I had a practical, total functional programming language to use for my day-to-day programming, and this non-termination everywhere is a great big blotch on Haskell’s claim of purity.
The kind of recursive types you get in Haskell is fine. They’re not great if you like the propositions-as-types interpretation, since it’s trivial to derive a contradiction from them, but they’re good enough for programming that implementing a positivity checker to ensure your definitions are strictly inductive isn’t generally worth the effort.
Unless your language claims to have “zero runtime errors”, in which case, if you implement isorecursive types instead of inductive types, you are wrong. See: Elm. God damn it.
So much for “no runtime errors”… I guess spinning forever on the client side is acceptable.
-- Elm type Void = Void Void type Omega = Omega (Omega -> Void) yesl : Omega yesl = Omega (\(Omega x) -> x (Omega x)) notl : Omega -> Void notl (Omega x) = x (Omega x)
Equirecursive types, however, are a totally different beast. They are basically useless. Sure, you might not have to write a couple of constructors, here and there… at the cost of dramatically increasing the set of incorrect programs that your type system accepts. Suddenly, typos will compile fine, and your program will just explode at runtime (more likely: fail to terminate). Isn’t this what type systems are meant to prevent?
Thankfully, very few languages implement equirecursive types. OCaml is the only one I know of, and it’s gated behind a compiler flag. However, that’s a footgun that should not be there.
EDIT (April 14th, 2021) It’s been pointed out to me that you can get equirecursive types in OCaml even without passing
-rectypes to the compiler. I am not an OCaml expert, so I encourage you to see here for more details.
The reason for the name will become obvious soon enough.↩︎