Imagine the following situation:
foo :: Show a => (a -> String) -> String -> Int -> IO ()
foo show' string int = do
putStrLn (show' string)
putStrLn (show' int)
Here, we want to pass in a function that converts a value into a String, apply that function to both a string parameter and and int parameter and print them both. In my mind, there is no reason this should fail! We have a function that works on both types of the parameters we're passing in.
Unfortunately, this won't type check! GHC infers the a
type based off of its first occurrence in the function body. That is, as soon as we hit:
putStrLn (show' string)
GHC will infer that show' :: String -> String
, since string
is a String
. It will proceed to blow up while trying to show' int
.
RankNTypes
lets you instead write the type signature as follows, quantifying over all functions that satisfy the show'
type:
foo :: (forall a. Show a => (a -> String)) -> String -> Int -> IO ()
This is rank 2 polymorphism: We are asserting that the show'
function must work for all a
s within our function, and the previous implementation now works.
The RankNTypes
extension allows arbitrary nesting of forall ...
blocks in type signatures. In other words, it allows rank N polymorphism.