1

I tried this code:

printing = print $ rank2 (+1)
rank2 :: Num n => (n -> n) -> Double
rank2 f = f 1.0

It threw the error:

Couldn't match expected type ‘Double’ with actual type ‘n’
‘n’ is a rigid type variable bound by the type signature

If I changed Double to Int, the error also changes from ‘Double’ to ‘Int’.

This error is only resolved if I use the RankNTypes GHC Language Extension:

{-# LANGUAGE RankNTypes #-}
printing = print $ rank2 (+1)
rank2 :: (forall n. Num n => n -> n) -> Double
rank2 f = f 1.0

I have read the answers at:

Understanding Haskell's RankNTypes

But I do not quite understand why this error would arise when I'm not using a tuple as the output, such as forall n. Num n => (n -> n) -> (Int, Double), where the tuple is (Int, Double). If a tuple is the output, the function f may face a conflict in choosing between the types Int or Double. But in this case, I'm using a single type output..

Is it due to the function f being rigidly polymorphic? If so, wouldn't this mean that being polymorphic is quite meaningless? The function can't adapt itself to a Double or Int..

maxloo
  • 453
  • 2
  • 12
  • "Is it due to the function f being rigidly polymorphic?" The word "rigid" in the error message means "user specified". As opposed to type variables generated by the compiler, which are called "wobbly". – Noughtmare Jun 03 '21 at 15:15
  • 1
    The function `f` is not polymorphic! Your first `rank2` is polymorphic, but its argument is *not*. ...and I think this is the absolute core, key misunderstanding. – Daniel Wagner Jun 03 '21 at 16:19

3 Answers3

4

So you want

rank1 :: Num n => (n -> n) -> Double
rank1 f = f 1.0

If this did typecheck, it would have to be possible to use it, for instance, like this:

bad :: Double
bad = rank1 (\n -> n + (sqrt (-1) :: Complex Double))

But that clearly can't work, because now you're adding an imaginary number to a real one and the result is supposed to be still a real Double.

With

rank2 :: (∀ n. Num n => n -> n) -> Double
rank2 f = f 1.0

you prevent this sort of usage, because the function you pass as the argument must be able to work for arbitrary number types, thus it's not possible for me to add a concrete Complex Double in the mix, only generic Num operations:

allright :: Double
allright = rank2 (\n -> n + abs (-1))
leftaroundabout
  • 117,950
  • 5
  • 174
  • 319
  • Thanks, I've been trying out variations of my code. Is this considered Rank-1 polymorphism: `printing = print $ rank1 (+1)`, `rank1 :: Num n => (n -> n) -> Int`, `rank1 f = 1`? Because it works. I find it hard to understand why the `n` variable, which is already typeclassed as `Num n`, does not associate itself with the type `Int` in the output when I use `rank1 f = f 1`? Using the terms from my reply to @DanielWagner, why do we need to consider the different actions taken by the caller and the callee in the first place? – maxloo Jun 05 '21 at 16:53
  • 1
    Because the signature explicitly requires that inside the `rank1` function, `n` should _not_ be “associated” (I think you mean _unified_) with any particular type, but left polymorphic. The point of this is precisely that the caller can choose what type `n` should be, which is the whole point of having a polymorphic function. If it's not a requirement that the caller gets to pick the type, you can as well omit the `Num` constraint and simply use `rank0 :: (Int -> Int) -> Int`; `rank0 f = f 1`. – leftaroundabout Jun 05 '21 at 18:11
4

This definition,

rank2 :: Num n => (n -> n) -> Double
rank2 f = f 1.0

Doesn't work because standard rank-1 polymorphism in Haskell says that the caller gets to choose what the type variables mean. It would be valid to call a function with that type with an argument f of type Int -> Int. This doesn't work with that definition because f needs to accept and return a Double.

When you make it higher-rank you resolve that by requiring f to be polymorphic over all n, rather than working on some type n that the caller chooses. Now it's possible to use f at the type Double -> Double in the body and get a result that type-checks.

This higher-rank type isn't completely useless in this case - you prevent the function being passed in from doing anything outside the Num type class. It can add, multiply, use literals, and a couple other things, but it can't divide or use trig functions, for instance.

Sometimes that kind of restriction on the capabilities of an input is really important, and so the higher-rank type provides value. runST :: (forall s. ST s a) -> a is an example of using a higher-rank type to prevent the input value from doing things that would break Haskell's evaluation model and result in buggy programs.

But I'm not sure you actually would care about that sort of thing in this case. It's probably easiest to just go with a rank-1 type that supports what you're doing more directly.

Carl
  • 26,500
  • 4
  • 65
  • 86
  • Thanks, however, the same error is thrown if I use `Int`. `rank2 :: Num n => (n -> n) -> Int`, `rank2 f = f 1`, except that in the error, the `Double` becomes `Int`.. – maxloo Jun 03 '21 at 16:11
  • 1
    @maxloo Yes, and the same reason applies, just swapping `Int` for `Double` (or `Float` or whatever): the caller gets to choose `n`, and if they choose an `n` different from `Int` (like `Double` or `Float` -- let's say `Double` for the rest of this comment), then `rank2` attempts to use the return value of a `Double -> Double` as its own return value even though it claims to return an `Int`. – Daniel Wagner Jun 03 '21 at 16:23
  • 1
    @DanielWagner, thanks, I realised I also do not fully understand what is meant by a caller? In my code, which is the caller and which is the callee? I believe both of these terms are from: https://www.schoolofhaskell.com/school/to-infinity-and-beyond/pick-of-the-week/guide-to-ghc-extensions/explicit-forall#rankntypes--rank2types--and-polymorphiccomponents – maxloo Jun 03 '21 at 16:31
  • 1
    @maxloo You might like [this answer of mine](https://stackoverflow.com/questions/42820603/why-can-a-num-act-like-a-fractional/42821578#42821578) with further details on one of the ways I think about polymorphism in Haskell. There I also use the term "caller", but hopefully in a more clear way -- it is in contrast to "implementer" rather than "callee". That is, the "implementer" is writing the code that has the given type, and the "caller" is using something that already exists and has the given type. – Daniel Wagner Jun 03 '21 at 17:05
  • @DanielWagner, thanks, I've read your replies at "Why can a Num act like a Fractional", but I'm not sure if I've understood the terms correctly, partly because I'm trying to link a implementer to a callee. For my code, can `rank2` be considered a caller? Then its type signature will be the protocol, and we can consider `f` a callee, which the implementer will start with on the right side of the equals sign. – maxloo Jun 05 '21 at 16:45
  • 1
    @maxloo No piece of code is a caller or an implementer -- those are always people writing code! When we are writing the code on the right hand side of the equals sign of `rank2 f = ...`, then we are the implementer of `rank2`, and possibly the caller of `f` if we like. I am okay with "its type signature will be the protocol", provided you're willing to be explicit about why it's okay to say "the protocol" instead of, say, "a protocol" -- there may be many protocols it makes sense to talk about it any given piece of code. But I'm okay with "`rank2`'s type signature is `rank2`'s protocol". – Daniel Wagner Jun 06 '21 at 01:38
  • 1
    @maxloo Another piece of precision I encourage you to pick up in your writing and thinking: "implementer" on its own is meaningless. You must always say what it is that's being implemented. So "implementer of `rank2`" is ok, for example. Similarly for "caller" -- on its own, it's not sensible, it must be in relation to some piece of code or protocol. (Sometimes context makes it clear which piece of code! But less often than you might think. Maybe less often than *I* might think, too. Let me know if you find it tricky to work out which piece of code is meant at each place in the linked answer.) – Daniel Wagner Jun 06 '21 at 01:41
3

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.

Jon Purdy
  • 53,300
  • 8
  • 96
  • 166