This:
rank1 :: Num n => (n -> n) -> Double
Is short for this:
rank1 :: forall n. Num n => (n -> n) -> Double
You can write an explicit forall
at the top level of a type signature using {-# Language ExplicitForAll #-}
. It’s also allowed with RankNTypes
, of course, but this is a rank-1 type.
This is the type of a polymorphic function with the following parameters:
A type n
, supplied implicitly by type inference or explicitly with {-# Language TypeApplications #-}
;
An instance of Num
for that type n
, also supplied implicitly by the compiler; and
A function (f
) from n
to n
.
The caller of rank1
specifies the argument values for each of these parameters.
Inside the definition of rank1
, it may call f
, but f
only has a single monomorphic type, being n -> n
for the particular type n
that the caller of rank1
specified.
For example, rank1 @Int
(using TypeApplications
syntax) has the type (Int -> Int) -> Double
, so in this case we would have f :: Int -> Int
. (More specifically, rank1 @Int
has type Num Int => (Int -> Int) -> Double
, but the constraint Num Int
is filled immediately by the fact that there is an instance Num Int
.)
Naturally, the Int
result of f
doesn’t match Double
in this case. In general, therefore, rank1
cannot assume anything about the type n
, except that it is in the class Num
. It promises to work “for all n
”. As such, there’s no way to get a Double
from an n
, because an instance Num t
doesn’t offer any conversions from the type t
, only to t
(with fromInteger :: Integer -> t
).
Contrast this with the rank-2 type in your question:
rank2 :: (forall n. Num n => n -> n) -> Double
Here, the forall
is nested within a function type … -> Double
. So rank2
is not polymorphic, and accepts only one parameter, which is a polymorphic function f
.
That function f
accepts three parameters: the type n
, the instance of Num
for n
, and a value of type n
. It returns a value of type n
.
So within the definition of rank2
, it may call f
, and as the caller of f
, rank2
may choose any n
that it wants, because the type of f
promises that it works “for all n
”. Therefore, rank2
can choose n
= Double
, which is valid because there is an instance Num Double
. The specialisation f @Double :: Double -> Double
returns a Double
, which can, of course, be returned from rank2
.
Since f
is polymorphic, rank2
could call it multiple times with different arguments for n
. That’s why you see examples using a tuple, like (forall n. Num n => n -> n) -> (Int, Double)
(note the position of the forall
). They’re demonstrating that you can call the functional parameter at multiple different types.