The type system of Haskell is based on Hindley-Milner, so it has so called let-bound polymorphism. It means identifiers bound using a let or where clause can be polymorphic. On the contrary, lambda-bound identifiers are monomorphic.
For example, the following program is illegal in Haskell:
foo :: (Int, Char)
foo = (\f -> (f 1, f 'a')) idThe Typing Pitfalls section of “A Gentle Introduction to Haskell, Version 98” also mentions a similar case.
let f g = (g [], g 'a') -- ill-typed expression
in f (\x->x)Thanks to let-polymorphism, we can easily make foo type-check by moving f to where clause.
foo :: (Int, Char)
foo = (f 1, f 'a')
where f = idOr by binding f with let.
foo :: (Int, Char)
foo = let f = id
in (f 1, f 'a')This is rather unfortunate because all these forms represent the same program, but only one of them fails to type check.
If Haskell can’t infer the type for us, let’s bite the bullet and perform the type inference by ourselves. What’s the type of f? It is forall a. a -> a. Thus the type of foo is (forall a. a -> a) -> (Int, Char).
Aha! This is a higher-rank type (rank 2 in this case) and we can specify the type of f using two GHC extensions; RankNTypes and ScopedTypeVariables.
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
foo :: (Int, Char)
foo = (\(f :: forall a. a -> a) -> (f 1, f 'a')) idRankNTypes allows us to express higher-rank types and ScopeTypeVariables allows free type variables to be re-used in the scope of a function
Now our program is well-typed!