Consider a variable introduced in a pattern, such as f
in this Haskell example:
case (\x -> x) of f -> (f True, f 'c')
This code results in a type error ("Couldn't match expected type ‘Bool’ with actual type ‘Char’"), because of the two different uses of f
. It shows that the inferred type of f
is not polymorphic in Haskell.
But why shouldn't f
be polymorphic?
I have two points of comparison: OCaml and "textbook" Hindley-Milner. Both suggest that f
ought to be polymorphic.
In OCaml, the analogous code is not an error:
match (fun x -> x) with f -> (f true, f 'c')
This evaluates to
(true, 'c')
with typebool * char
. So it looks like OCaml gets along fine with assigningf
a polymorphic type.We can gain clarity by stripping things down to the fundamentals of Hindley-Milner - lambda calculus with "let" - which both Haskell and OCaml are based on. When reduce to this core system, of course, there is no such thing as pattern matching. We can draw parallels though. Between "let" and "lambda",
case expr1 of f -> expr2
is much closer tolet f = expr1 in expr2
than to(lambda f. expr2) expr1
. "Case", like "let", syntactically restrictsf
to be bound toexpr1
, while a functionlambda f. expr2
doesn't know whatf
will be bound to since the function has no such restriction on where in the program it will be called. This was the reason why let-bound variables are generalized in Hindley-Milner and lambda-bound variables are not. It appears that the same reasoning that allows let-bound variables to be generalized shows that variables introduced by pattern matching could be generalized too.
The examples above are minimal for clarity, so they only show a trivial pattern f
in the pattern matching, but all the same logic extends to arbitrarily complex patterns like Just (a:b:(x,y):_)
, which can introduce multiple variables that would all be generalized.
Is my analysis correct? In Haskell specifically - recognizing that it's not just plain Hindley-Milner and not OCaml - why don't we generalize the type of f
in the first example?
Was this an explicit language design decision, and if so, what were the reasons? (I note that some in the community think that not even "let" should be generalized, but I would imagine the design decision pre-dates that paper.)
If variables introduced in a pattern were made polymorphic similar to "let", would that break compatibility with other aspects of Haskell in a significant way?
newtype PolyEndo = Endo {runEndo :: ∀ a . a -> a}
, you can writecase Endo id of Endo f -> (f True, f 'a')
. That makes it hardly less strange that it doesn't work without the wrapper, though... - leftaroundabout