Higher rank polymorphism

Posted on December 27, 2016 by Kwang Yul Seo

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!