I've searched around the Internet, and I can't find any explanations of CHI which don't rapidly degenerate into a lecture on logic theory which is drastically over my head. (These people talk as if "intuitionistic proposition calculus" is a phrase that actually means something to normal humans!)
Roughly put, CHI says that types are theorems, and programs are proofs of those theorems. But what the hell does that even mean??
So far, I've figured this out:
Consider id :: x -> x
. Its type says "given that X is true, we can conclude that X is true". Seems like a reasonable theorem to me.
Now consider foo :: x -> y
. As any Haskell programmer will tell you, this is impossible. You cannot write this function. (Well, without cheating anyway.) Read as a theorem, it says "given that any X is true, we can conclude that any Y is true". This is obviously nonsense. And, sure enough, you cannot write this function.
More generally, the function's arguments can be considered "this which are assumed to be true", and the result type can be considered "thing that is true assuming all the other things are". If there's a function argument, say x -> y
, we can take that as an assumption that X being true implies that Y must be true.
For example, (.) :: (y -> z) -> (x -> y) -> x -> z
can be taken as "assuming that Y implies Z, that X implies Y, and that X is true, we can conclude that Z is true". Which seems logically sensible to me.
Now, what the hell does Int -> Int
mean?? o_O
The only sensible answer I can come up with is this: If you have a function X -> Y -> Z, then the type signature says "assuming that it's possible to construct a value of type X, and another of type Y, then it is possible to construct a value of type Z". And the function body describes exactly how you would do this.
That seems to make sense, but it's not very interesting. So clearly there must be more to it than this...
The Curry-Howard isomorphism simply states that types correspond to propositions, and values correspond to proofs.
Int -> Int
doesn't really mean much interesting as a logical proposition. When interpreting something as a logical proposition, you're only interested in whether the type is inhabited (has any values) or not. So, Int -> Int
just means "given an Int
, I can give you an Int
", and it is, of course, true. There are many different proofs of it (corresponding to various different functions of that type), but when taking it as a logical proposition, you don't really care.
That doesn't mean interesting propositions can't involve such functions; just that that particular type is quite boring, as a proposition.
For an instance of a function type that isn't completely polymorphic and that has logical meaning, consider p -> Void
(for some p), where Void
is the uninhabited type: the type with no values (except ⊥, in Haskell, but I'll get to that later). The only way to get a value of type Void
is if you can prove a contradiction (which is, of course, impossible), and since Void
means you've proved a contradiction, you can get any value from it (i.e. there exists a function absurd :: Void -> a
). Accordingly, p -> Void
corresponds to ¬p: it means "p implies falsehood".
Intuitionistic logic is just a certain kind of logic that common functional languages correspond to. Importantly, it's constructive: basically, a proof of a -> b
gives you an algorithm to compute b
from a
, which isn't true in regular classical logic (because of the law of excluded middle, which will tell you that something is either true or false, but not why).
Even though functions like Int -> Int
don't mean much as propositions, we can make statements about them with other propositions. For instance, we can declare the type of equality of two types (using a GADT):
data Equal a b where Refl :: Equal a a
If we have a value of type Equal a b
, then a
is the same type of b
: Equal a b
corresponds to the proposition a = b. The problem is that we can only talk about equality of types this way. But if we had dependent types, we could easily generalise this definition to work with any value, and so Equal a b
would correspond to the proposition that the values a and b are identical. So, for instance, we could write:
type IsBijection (f :: a -> b) (g :: b -> a) = forall x. Equal (f (g x)) (g (f x))
Here, f and g are regular functions, so f could easily have type Int -> Int
. Again, Haskell can't do this; you need dependent types to do things like this.
Typical functional languages are not really well-suited to writing proofs, not only because they lack dependent types, but also because of ⊥, which, having type a
for all a
, acts as a proof of any proposition. But total languages like Coq and Agda exploit the correspondence to act as proof systems as well as dependently-typed programming languages.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With