One of my favorite features of the Hindley-Milner type system is the built-in exhaustiveness checking that is applied to pattern matches. I like this feature enough that it is the focus of the only worked-out example I give in my talk about OCaml at Jane Street.
Why is exhaustivenss checking so important? You can slog through my talk if you want to hear a fuller account, but the basic point is that case analysis is one of the most common things one does in any program, and anything that can statically check important properties of your case analysis is really helpful. Moreover, exhaustiveness checking serves as a kind of refactoring tool. Whenever you expand the possibilities in a type, the compiler will point you to the places where you forgot to handle the new cases you just created. It is, from the perspective of someone who has programmed in ML for a living for some years now, an enormously useful feature.
So I was quite taken aback when one of our interns here (who will remain nameless, in case I’ve horribly misconstrued his words) pointed out to me that Haskell by default doesn’t even warn the programmer about inexhaustive pattern-matches. In particular, if you save the following code into a file called ‘foo.hs’:
foo (Just x) = x + 1 main = print (foo Nothing)
and the compile it, the compiler won’t make a peep. But when you run it, you will of course get a runtime error:
bash-3.2$ ghc -o haskell-foo foo.hs bash-3.2$ ./haskell-foo haskell-foo: foo.hs:1:0-19: Non-exhaustive patterns in function foo
If you try something similar in OCaml:
let foo (Some x) = x + 1 let () = Printf.printf "%d\n" (foo None)
you’ll get an earful from the compiler when you try to build it:
bash-3.2$ ocamlc -o ocaml-foo foo.ml File "foo.ml", line 1, characters 8-24: Warning P: this pattern-matching is not exhaustive. Here is an example of a value that is not matched: None
We go even farther at Jane Street, where we use compiler flags to turn that warning into an error, so that the code can’t build when the match is incomplete. When people want an incomplete match, they need to do it explicitly by adding a catch-all case, and to throw an explicit exception, as follows:
let foo = function Some x -> x + 1 | None -> failwith "Argh!" let () = Printf.printf "%d\n" (foo None)
It is worth noting that all is not wine and roses in the ML world. Many, maybe most, ML programmers basically ignore the compiler’s warnings about inexhaustive matches, which makes the warning the compiler does give you kind of useless. Still, the lack of even an admonition from ghc surprises me.
Presumably Haskell programmers are just as concerned with getting static guarantees as ML programmers are. So I’m wondering why the difference in the default compiler behavior. I’d be interested in hearing from any Haskell programmers who can explain what’s going on.