18

While working my way through GHC extensions, I came across RankNTypes at the School of Haskell, which had the following example:

main = print $ rankN (+1)

rankN :: (forall n. Num n => n -> n) -> (Int, Double)
rankN f = (f 1, f 1.0)

The article offered an alternative type for rankN:

rankN :: forall n. Num n => (n -> n) -> (Int, Double) 

The explanation of the difference is that

The latter signature requires a function from n to n for some Num n; the former signature requires a function from n to n for every Num n.

I can understand that the former type requires a signature to be what's in the parentheses or more general. I do not understand the explanation that the latter signature simply requires a function n -> n for "some Num n". Can someone elaborate and clarify? How do you "read" this former signature so that it sounds like what it means? Is the latter signature the same as simply Num n => (n -> n) -> (Int, Double) without the need for forall?

Abhijit Sarkar
  • 21,927
  • 20
  • 110
  • 219
  • 3
    Think of the body of the function as: `(f (1 :: Int), f (1.0 :: Double))`. You cannot use the latter type signature for this. In `Num n => n -> n -> (Int, Double)` you'd have that `n` must be both `Int` and `Double` *at the same time*. Using `(forall n . Num n => n -> n) -> (Int, Double)` you have that the function `f` can be applied to different types and thus it is well typed. – Bakuriu Oct 31 '15 at 07:00

3 Answers3

20

In the normal case (forall n. Num n => (n -> n) -> (Int, Double)), we choose an n first and then provide a function. So we could pass in a function of type Int -> Int, Double -> Double, Rational -> Rational and so on.

In the Rank 2 case ((forall n. Num n => n -> n) -> (Int, Double)) we have to provide the function before we know n. This means that the function has to work for any n; none of the examples I listed for the previous example would work.

We need this for the example code given because the function f that's passed in is applied to two different types: an Int and a Double. So it has to work for both of them.

The first case is normal because that's how type variables work by default. If you don't have a forall at all, your type signature is equivalent to having it at the very beginning. (This is called prenex form.) So Num n => (n -> n) -> (Int, Double) is implicitly the same as forall n. Num n => (n -> n) -> (Int, Double).

What's the type of a function that works for any n? It's exactly forall n. Num n => n -> n.

Tikhon Jelvis
  • 67,485
  • 18
  • 177
  • 214
  • 1
    Thank you. In "Rank N", what does rank mean and what does the N refer to? –  Oct 31 '15 at 01:23
  • 3
    The `N` refers to how deeply you can nest `forall` inside functions (`->`). In this case, it's nested inside a single function, so it would be rank 2. Haskell used to *only* support Rank2 polymorphism but since it now supports arbitrary ranks, this is more of a historical note. – Tikhon Jelvis Oct 31 '15 at 01:46
9

How do you "read" this former signature so that it sounds like what it means?

You can read

rankN :: (forall n. Num n => n -> n) -> (Int, Double)

as "rankN takes a parameter f :: Num n => n -> n" and returns (Int, Double), where f :: Num n => n -> n can be read as "for any numeric type n, f can take an n and return an n".

The rank one definition

rank1 :: forall n. Num n => (n -> n) -> (Int, Double)

would then be read as "For any numeric type n, rank1 takes an argument f :: n -> n and returns an (Int, Double)".

Is the latter signature the same as simply Num n => (n -> n) -> (Int, Double) without the need for forall?

Yes, by default all foralls are implicitly placed at the outer-most position (resulting in a rank-1 type).

sepp2k
  • 363,768
  • 54
  • 674
  • 675
  • 2
    More than a half-decade late, but I want to tell you that this explanation is one of the best intuition-pumps for Rank2Types. – AgentLiquid Jan 11 '21 at 08:08
8

In the rankN case f has to be a polymorphic function which is valid for all numeric types n.

In the rank1 case f only has to be defined for a single numeric type.

Here is some code which illustrates this:

{-# LANGUAGE RankNTypes #-}

rankN :: (forall n. Num n => n -> n) -> (Int, Double)
rankN = undefined

rank1 :: forall n. Num n => (n -> n) -> (Int, Double)
rank1 = undefined

foo :: Int -> Int  -- monomorphic
foo n = n + 1

test1 = rank1 foo -- OK

test2 = rankN foo -- does not type check

test3 = rankN (+1) -- OK since (+1) is polymorphic

Update

In response to @helpwithhaskell's question in the comments...

Consider this function:

bar :: (forall n. Num n => n -> n) -> (Int, Double) -> (Int, Double)
bar f (i,d) = (f i, f d)

That is, we apply f to both an Int and a Double. Without using RankNTypes it won't type check:

-- doesn't work
bar' :: ??? -> (Int, Double) -> (Int, Double)
bar' f (i,d) = (f i, f d)

None of the following signatures work for ???:

Num n => (n -> n)
Int -> Int
Double -> Double
ErikR
  • 51,541
  • 9
  • 73
  • 124