120

I am not really proficient in Haskell, so this might be a very easy question.

What language limitation do Rank2Types solve? Don't functions in Haskell already support polymorphic arguments?

Chris Martin
  • 30,334
  • 10
  • 78
  • 137
Andrey Shchekin
  • 21,101
  • 19
  • 94
  • 162
  • It's basically an upgrade from HM type system to polymorphic lambda calculus aka. λ2/System F . Keep in mind that type inference undecidable in λ2. – Poscat Apr 02 '20 at 09:12

5 Answers5

180

It's hard to understand higher-rank polymorphism unless you study System F directly, because Haskell is designed to hide the details of that from you in the interest of simplicity.

But basically, the rough idea is that polymorphic types don't really have the a -> b form that they do in Haskell; in reality, they look like this, always with explicit quantifiers:

id :: ∀a.a → a
id = Λt.λx:t.x

If you don't know the "∀" symbol, it's read as "for all"; ∀x.dog(x) means "for all x, x is a dog." "Λ" is capital lambda, used for abstracting over type parameters; what the second line says is that id is a function that takes a type t, and then returns a function that's parametrized by that type.

You see, in System F, you can't just apply a function like that id to a value right away; first you need to apply the Λ-function to a type in order to get a λ-function that you apply to a value. So for example:

(Λt.λx:t.x) Int 5 = (λx:Int.x) 5
                  = 5

Standard Haskell (i.e., Haskell 98 and 2010) simplifies this for you by not having any of these type quantifiers, capital lambdas and type applications, but behind the scenes GHC puts them in when it analyzes the program for compilation. (This is all compile-time stuff, I believe, with no runtime overhead.)

But Haskell's automatic handling of this means that it assumes that "∀" never appears on the left-hand branch of a function ("→") type. Rank2Types and RankNTypes turn off those restrictions and allow you to override Haskell's default rules for where to insert forall.

Why would you want to do this? Because the full, unrestricted System F is hella powerful, and it can do a lot of cool stuff. For example, type hiding and modularity can be implemented using higher-rank types. Take for example a plain old function of the following rank-1 type (to set the scene):

f :: ∀r.∀a.((a → r) → a → r) → r

To use f, the caller first must choose what types to use for r and a, then supply an argument of the resulting type. So you could pick r = Int and a = String:

f Int String :: ((String → Int) → String → Int) → Int

But now compare that to the following higher-rank type:

f' :: ∀r.(∀a.(a → r) → a → r) → r

How does a function of this type work? Well, to use it, first you specify which type to use for r. Say we pick Int:

f' Int :: (∀a.(a → Int) → a → Int) → Int

But now the ∀a is inside the function arrow, so you can't pick what type to use for a; you must apply f' Int to a Λ-function of the appropriate type. This means that the implementation of f' gets to pick what type to use for a, not the caller of f'. Without higher-rank types, on the contrary, the caller always picks the types.

What is this useful for? Well, for many things actually, but one idea is that you can use this to model things like object-oriented programming, where "objects" bundle some hidden data together with some methods that work on the hidden data. So for example, an object with two methods—one that returns an Int and another that returns a String, could be implemented with this type:

myObject :: ∀r.(∀a.(a → Int, a -> String) → a → r) → r

How does this work? The object is implemented as a function that has some internal data of hidden type a. To actually use the object, its clients pass in a "callback" function that the object will call with the two methods. For example:

myObject String (Λa. λ(length, name):(a → Int, a → String). λobjData:a. name objData)

Here we are, basically, invoking the object's second method, the one whose type is a → String for an unknown a. Well, unknown to myObject's clients; but these clients do know, from the signature, that they will be able to apply either of the two functions to it, and get either an Int or a String.

For an actual Haskell example, below is the code that I wrote when I taught myself RankNTypes. This implements a type called ShowBox which bundles together a value of some hidden type together with its Show class instance. Note that in the example at the bottom, I make a list of ShowBox whose first element was made from a number, and the second from a string. Since the types are hidden by using the higher-rank types, this doesn't violate type checking.

{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ImpredicativeTypes #-}

type ShowBox = forall b. (forall a. Show a => a -> b) -> b

mkShowBox :: Show a => a -> ShowBox
mkShowBox x = \k -> k x

-- | This is the key function for using a 'ShowBox'.  You pass in
-- a function @k@ that will be applied to the contents of the 
-- ShowBox.  But you don't pick the type of @k@'s argument--the 
-- ShowBox does.  However, it's restricted to picking a type that
-- implements @Show@, so you know that whatever type it picks, you
-- can use the 'show' function.
runShowBox :: forall b. (forall a. Show a => a -> b) -> ShowBox -> b
-- Expanded type:
--
--     runShowBox 
--         :: forall b. (forall a. Show a => a -> b) 
--                   -> (forall b. (forall a. Show a => a -> b) -> b)
--                   -> b
--
runShowBox k box = box k


example :: [ShowBox] 
-- example :: [ShowBox] expands to this:
--
--     example :: [forall b. (forall a. Show a => a -> b) -> b]
--
-- Without the annotation the compiler infers the following, which
-- breaks in the definition of 'result' below:
--
--     example :: forall b. [(forall a. Show a => a -> b) -> b]
--
example = [mkShowBox 5, mkShowBox "foo"]

result :: [String]
result = map (runShowBox show) example

PS: for anybody reading this who's wondered how come ExistentialTypes in GHC uses forall, I believe the reason is because it's using this sort of technique behind the scenes.

Luis Casillas
  • 29,802
  • 7
  • 49
  • 102
  • 2
    Thanks for a very elaborate answer! (which, incidentally, also finally motivated me to learn proper type theory and System F.) – Aleksandar Dimitrov Aug 20 '12 at 07:52
  • 5
    If you had `exists` keyword, you could define an existential type as (for example) `data Any = Any (exists a. a)`, where `Any :: (exists a. a) -> Any`. Using ∀x.P(x) → Q ≡ (∃x.P(x)) → Q, we can conclude that `Any` could also have a type `forall a. a -> Any` and that's where the `forall` keyword comes from. I believe that existential types as implemented by GHC are just ordinary data types which also carry all the required typeclass dictionaries (I couldn't find a reference to back this up, sorry). – Vitus Aug 20 '12 at 19:46
  • 2
    @Vitus: GHC existentials aren't tied to typeclass dictionaries. You can have `data ApplyBox r = forall a. ApplyBox (a -> r) a`; when you pattern match to `ApplyBox f x`, you get `f :: h -> r` and `x :: h` for a "hidden," restricted type `h`. If I understand right, the typeclass dictionary case is translated into something like this: `data ShowBox = forall a. Show a => ShowBox a` is translated to something like `data ShowBox' = forall a. ShowBox' (ShowDict' a) a`; `instance Show ShowBox' where show (ShowBox' dict val) = show' dict val`; `show' :: ShowDict a -> a -> String`. – Luis Casillas Aug 20 '12 at 23:01
  • That is a great answer I will have to spend some time on. I think I am too used to the abstractions C# generics provide, so I was taking a lot of that for granted instead of actually understanding the theory. – Andrey Shchekin Aug 21 '12 at 00:20
  • @sacundim: Well, "all required typeclass dictionaries" can also mean no dictionaries at all if you don't need any. :) My point was that GHC most likely doesn't encode existential types via higher ranked types (i.e. the transformation you suggest - ∃x.P(x) ~ ∀r.(∀x.P(x) → r) → r). – Vitus Aug 21 '12 at 01:22
  • Just wondering why the use of the term "rank"? Why did they choose to call it "higher rank"? – CMCDragonkai Apr 13 '15 at 10:35
  • @LuisCasillas Is polymorphic parameted `b` in type ShowBox = forall b. (forall a. Show a => a -> b) -> b is really necessary? I think it would be more clean if you just write: `type ShowBox = (forall a. Show a => a -> String) -> String` `runShowBox :: (forall a. Show a => a -> String) -> ShowBox -> String` or provide additional example where polimorphic `b` has practical use (showsPrec perhaps?) Otherwise it just introduces unnecessary complexity, not needed for understanding higher-rank types. – schernichkin Mar 31 '16 at 13:56
124

Do not functions in Haskell already support polymorphic arguments?

They do, but only of rank 1. This means that while you can write a function that takes different types of arguments without this extension, you can't write a function that uses its argument as different types in the same invocation.

For example the following function can't be typed without this extension because g is used with different argument types in the definition of f:

f g = g 1 + g "lala"

Note that it's perfectly possible to pass a polymorphic function as an argument to another function. So something like map id ["a","b","c"] is perfectly legal. But the function may only use it as monomorphic. In the example map uses id as if it had type String -> String. And of course you can also pass a simple monomorphic function of the given type instead of id. Without rank2types there is no way for a function to require that its argument must be a polymorphic function and thus also no way to use it as a polymorphic function.

sepp2k
  • 363,768
  • 54
  • 674
  • 675
  • 6
    To add some words connecting my answer to this one: consider the Haskell function `f' g x y = g x + g y`. Its inferred rank-1 type is `forall a r. Num r => (a -> r) -> a -> a -> r`. Since `forall a` is outside the function arrows, the caller must first pick a type for `a`; if they pick `Int`, we get `f' :: forall r. Num r => (Int -> r) -> Int -> Int -> r`, and now we have fixed the `g` argument so it can take `Int` but not `String`. If we enable `RankNTypes` we can annotate `f'` with type `forall b c r. Num r => (forall a. a -> r) -> b -> c -> r`. Can't use it, though—what would `g` be? – Luis Casillas Aug 31 '12 at 00:34
55

Luis Casillas's answer gives a lot of great info about what rank 2 types mean, but I'll just expand on one point he didn't cover. Requiring an argument to be polymorphic doesn't just allow it to be used with multiple types; it also restricts what that function can do with its argument(s) and how it can produce its result. That is, it gives the caller less flexibility. Why would you want to do that? I'll start with a simple example:

Suppose we have a data type

data Country = BigEnemy | MediumEnemy | PunyEnemy | TradePartner | Ally | BestAlly

and we want to write a function

f g = launchMissilesAt $ g [BigEnemy, MediumEnemy, PunyEnemy]

that takes a function that's supposed to choose one of the elements of the list it's given and return an IO action launching missiles at that target. We could give f a simple type:

f :: ([Country] -> Country) -> IO ()

The problem is that we could accidentally run

f (\_ -> BestAlly)

and then we'd be in big trouble! Giving f a rank 1 polymorphic type

f :: ([a] -> a) -> IO ()

doesn't help at all, because we choose the type a when we call f, and we just specialize it to Country and use our malicious \_ -> BestAlly again. The solution is to use a rank 2 type:

f :: (forall a . [a] -> a) -> IO ()

Now the function we pass in is required to be polymorphic, so \_ -> BestAlly won't type check! In fact, no function returning an element not in the list it is given will typecheck (although some functions that go into infinite loops or produce errors and therefore never return will do so).

The above is contrived, of course, but a variation on this technique is key to making the ST monad safe.

Community
  • 1
  • 1
dfeuer
  • 48,079
  • 5
  • 63
  • 167
  • Great. Great. I seem to understand a little, can I rephrase the function requirement for all a.[a]->a is it correct to say, that this function is allowed to use a list of 'a' but it has to produce some a that is part of this list not some other instance of type a. I don't understand why this is said to be polymorphic? It's a restriction of polymorphism... – user3680029 Jul 27 '21 at 14:23
  • @user3680029, the *user* of a (rank-1) polymorphic function has *more* options. The *writer* of a polymorphic function has *fewer* options. – dfeuer Jul 27 '21 at 14:27
  • Is this like Julia dynamic dispatch? The callee has to fit the demanded type not less and not more than needed – user3680029 Jul 27 '21 at 23:49
  • @user3680029, I don't know Julia. Here the caller must provide no less than demanded. They may provide a type that subsumes the demanded one. – dfeuer Jul 28 '21 at 00:24
  • Another try is this why ST monad works? Especially runST you cannot pass around values belonging to different threaded states. The list of values is kind of like that thread? It's capturing some values which you can't escape... – user3680029 Aug 07 '21 at 08:06
  • @user3680029, I don't really know what you're getting at. Maybe you should ask a separate question? – dfeuer Aug 07 '21 at 15:38
  • sorry but it didn't work when add BestAlly was added to the list f ::(forall a . [a] -> a) -> IO () f g = launchMissilesAt $ g [BigEnemy, MediumEnemy, PunyEnemy, BestAlly] then f (\ _ -> BestAlly) => Couldn't match expected type `a' with actual type `Country' ... without BestAlly in the provided list it works. Why? – user3680029 Aug 14 '22 at 09:18
  • @user3680029 that's kind of the whole point. You can't use any particular one; you have to select one from the list you're given. – dfeuer Aug 17 '22 at 00:00
  • right f last worked - still need to wrap my head around this.. thanx – user3680029 Aug 17 '22 at 15:13
22

Higher-rank types aren't as exotic as the other answers have made out. Believe it or not, many object-oriented languages (including Java and C#!) feature them. (Of course, no one in those communities knows them by the scary-sounding name "higher-rank types".)

The example I'm going to give is a textbook implementation of the Visitor pattern, which I use all the time in my daily work. This answer is not intended as an introduction to the visitor pattern; that knowledge is readily available elsewhere.

In this fatuous imaginary HR application, we wish to operate on employees who may be full-time permanent staff or temporary contractors. My preferred variant of the Visitor pattern (and indeed the one which is relevant to RankNTypes) parameterises the visitor's return type.

interface IEmployeeVisitor<T>
{
    T Visit(PermanentEmployee e);
    T Visit(Contractor c);
}

class XmlVisitor : IEmployeeVisitor<string> { /* ... */ }
class PaymentCalculator : IEmployeeVisitor<int> { /* ... */ }

The point is that a number of visitors with different return types can all operate on the same data. This means IEmployee must express no opinion as to what T ought to be.

interface IEmployee
{
    T Accept<T>(IEmployeeVisitor<T> v);
}
class PermanentEmployee : IEmployee
{
    // ...
    public T Accept<T>(IEmployeeVisitor<T> v)
    {
        return v.Visit(this);
    }
}
class Contractor : IEmployee
{
    // ...
    public T Accept<T>(IEmployeeVisitor<T> v)
    {
        return v.Visit(this);
    }
}

I wish to draw your attention to the types. Observe that IEmployeeVisitor universally quantifies its return type, whereas IEmployee quantifies it inside its Accept method - that is to say, at a higher rank. Translating clunkily from C# to Haskell:

data IEmployeeVisitor r = IEmployeeVisitor {
    visitPermanent :: PermanentEmployee -> r,
    visitContractor :: Contractor -> r
}

newtype IEmployee = IEmployee {
    accept :: forall r. IEmployeeVisitor r -> r
}

So there you have it. Higher-rank types show up in C# when you write types containing generic methods.

Community
  • 1
  • 1
Benjamin Hodgson
  • 42,952
  • 15
  • 108
  • 157
  • 2
    I'd be keen to know if anyone else has written about C#/Java/Blub's support for higher-rank types. If you, dear reader, know of any such resources please send them my way! – Benjamin Hodgson Jan 06 '16 at 00:47
-4

For those familiar with object oriented languages, a higher-rank function is simply a generic function that expects as its argument another generic function.

E.g. in TypeScript you could write:

type WithId<T> = T & { id: number }
type Identifier = <T>(obj: T) => WithId<T>
type Identify = <TObj>(obj: TObj, f: Identifier) => WithId<TObj>

See how the generic function type Identify demands a generic function of the type Identifier? This makes Identify a higher-rank function.

Asad Saeeduddin
  • 46,193
  • 6
  • 90
  • 139
  • What does this add to sepp2k's answer? – dfeuer Jul 27 '17 at 17:03
  • Or Benjamin Hodgson's, for that matter? – dfeuer Jul 27 '17 at 17:41
  • @dfeuer It states the concept of higher rank types in terminology that is more familiar to those who may not be familiar with Haskell or System F. Also, Benjamin Hodgson's answer isn't an actual example of higher rank types as far as I can see; `IEmployee` accepts the *monomorphic* type `IEmployeeVisitor`. The type `IEmployeeVisitor` has been fully applied. It is only higher rank typing if you are accepting open polymorphic types. – Asad Saeeduddin Jul 27 '17 at 17:53
  • 1
    I think you missed Hodgson's point. `Accept` has a rank-1 polymorphic type, but it's a method of `IEmployee`, which is itself rank-2. If someone gives me an `IEmployee`, I can open it up and use its `Accept` method at any type. – dfeuer Jul 27 '17 at 18:00
  • @dfeuer You're wrong. A higher rank type occurs when the `forall` occurs in a position where it can't be floated out to the LHS, i.e. in an embedded open polymorphic type. In Benjamin's example all polymorphic types are rank 1: in fact I can write his example in Haskell without using `Rank2Types` or any extensions at all for that matter. Here: https://gist.github.com/masaeedu/30adb29ad4b1d0cfd1ae65aaedf7ad9e – Asad Saeeduddin Jul 27 '17 at 21:14
  • 1
    Your example is also rank-2, by way of the `Visitee` class you introduce. A function `f :: Visitee e => T e` is (once the class stuff is desugared) essentially `f :: (forall r. e -> Visitor e r -> r) -> T e`. Haskell 2010 lets you get away with limited rank-2 polymorphism using classes like that. – dfeuer Jul 27 '17 at 22:33
  • @dfeuer Simply having a `forall` in the type doesn't make it rank 2. It's only rank 2 if you *can't* float it out to the left like that. A simple function type definition like `id :: x -> x` implicitly has a `forall` in it, i.e. properly speaking it's `id :: forall x. x -> x`, but that doesn't make it a rank 2 type. – Asad Saeeduddin Jul 27 '17 at 23:50
  • @dfeuer Do you have some docs pointing to typeclasses introducing rank 2 polymorphism? From my understanding HM deliberately limits rank 2 polymorphism because it introduces undecidability, that's why turning it on is an opt in language extension. – Asad Saeeduddin Jul 27 '17 at 23:53
  • 1
    You can't float out the `forall` in my example. I don't have a reference off hand, but you may well find something in ["Scrap Your Type Classes"](http://www.haskellforall.com/2012/05/scrap-your-type-classes.html?m=1). Higher-rank polymorphism can indeed introduce type-checking problems, but the limited sort implicit in the class system is fine. – dfeuer Jul 28 '17 at 00:32