Functional Programming – Is Higher-Rank Parametric Polymorphism Useful?

functional programminggenericsprogramming-languagestype-systems

I'm pretty sure everyone is familiar with generic methods of the form:

T DoSomething<T>(T item)

This function is also called parametrically polymorphic (PP), specifically rank-1 PP.

Let's say this method can be represented using a function object of the form:

<T> : T -> T

That is, <T> means it takes one type parameter, and T -> T means that it takes one parameter of type T and returns a value of the same type.

Then the following would be a rank-2 PP function:

(<T> : T -> T) -> int 

The function takes no type parameters itself, but takes a function which takes a type parameter. You can continue this iteratively, making the nesting deeper and deeper, getting PP of higher and higher rank.

This feature is really rare among programming languages. Even Haskell doesn't allow it by default.

Is it useful? Can it describe behaviors that are difficult to describe otherwise?

Also, what does it mean for something to be impredicative? (in this context)

Best Answer

In general, you use higher-rank polymorphism when you want the callee to be able to select the value of a type parameter, rather than the caller. For example:

f :: (forall a. Show a => a -> Int) -> (Int, Int)
f g = (g "one", g 2)

Any function g that I pass to this f must be able to give me an Int from a value of some type, where the only thing g knows about that type is that it has an instance of Show. So these are kosher:

f (length . show)
f (const 42)

But these are not:

f length
f succ

One particularly useful application is in using the scoping of types to enforce the scoping of values. Suppose we have an object of type Action<T>, representing an action we can run to produce a result of type T, such as a future or callback.

T runAction<T>(Action<T>)

runAction :: forall a. Action a -> a

Now, suppose that we also have an Action that can allocate Resource<T> objects:

Action<Resource<T>> newResource<T>(T)

newResource :: forall a. a -> Action (Resource a)

We want to enforce that those resources are only used inside the Action where they were created, and not shared between different actions or different runs of the same action, so that actions are deterministic and repeatable.

We can use higher-ranked types to accomplish this by adding a parameter S to the Resource and Action types, which is totally abstract—it represents the “scope” of the Action. Now our signatures are:

T run<T>(<S> Action<S, T>)
Action<S, Resource<S, T>> newResource<T>(T)

runAction :: forall a. (forall s. Action s a) -> a
newResource :: forall s a. a -> Action s (Resource s a)

Now when we give runAction an Action<S, T>, we are assured that because the “scope” parameter S is fully polymorphic, it cannot escape the body of runAction—so any value of a type that uses S such as Resource<S, int> likewise cannot escape!

(In Haskell, this is known as the ST monad, where runAction is called runST, Resource is called STRef, and newResource is called newSTRef.)

Related Topic