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')) id
```

The 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 = id
```

Or 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')) id
```

`RankNTypes`

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!