Skip to content

Instantly share code, notes, and snippets.

@jship
Last active May 24, 2018 15:51
Show Gist options
  • Save jship/bb5deccc432df79d69102da1312602ff to your computer and use it in GitHub Desktop.
Save jship/bb5deccc432df79d69102da1312602ff to your computer and use it in GitHub Desktop.
RankNTypes intuition

A coworker and I were discussing RankNTypes and why the extension is used in lens. RankNTypes has confused me for a while, but making the connection to lens gave me a good intuition (strangely?). Here’s a recap in case it’s useful to anyone else:

If you have a normal higher-order function:

foo :: Num a => (a -> a) -> a
foo f = f 1

We can pass foo any function that fits the (a -> a) with a Num constraint:

foo id
1

foo abs
1

foo negate
-1

But what if we want to define foo such that foo itself can decide the type of a, since it knows it’s given a function that will work for any a as long as it is a Num. We’ll try:

foo' :: Num a => (a -> a) -> a
foo' f = f (1 :: Int)
<interactive>:23:13: error:
• Couldn't match expected type 'a' with actual type 'Int'
'a' is a rigid type variable bound by
the type signature for:
foo' :: forall a. Num a => (a -> a) -> a
at <interactive>:22:9
• In the first argument of 'f', namely '(1 :: Int)'
In the expression: f (1 :: Int)
In an equation for 'foo'': foo' f = f (1 :: Int)
• Relevant bindings include
f :: a -> a (bound at <interactive>:23:6)
foo' :: (a -> a) -> a (bound at <interactive>:23:1)

Womp womp, no can do. This makes sense too because the return type of the function is a. Changing it to Int would also give us a compiler error. But what if we instead make the function passed into the function we’re trying to define be Rank 2 polymorphic?

:set -XRank2Types
foo' :: (forall a. Num a => (a -> a)) -> Int
foo' f = f (1 :: Int)

Now the implementor of foo' gets the ability to specialize the concrete type to Int. Squint hard enough, and foo' is very similar to lens library’s view, set, etc:

type Setter s t a b = forall f. Settable f => (a -> f b) -> s -> f t
set :: Setter s t a b -> b -> s -> t
-- ... aka ...
set :: (forall f. Settable f => (a -> f b) -> s -> f t) -> b -> s -> t

The set function can choose what specialization it wants for f as long as it is Settable, so it uses the ASetter type synonym which uses Identity for f:

type ASetter s t a b = (a -> Identity b) -> s -> Identity t

In my coworker’s words, RankNTypes let us have “multiple specializations at different call sites”.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment