uncurry id == uncurry ($)

haskell applicative apply maps

The other day I was trying to find if joining two Map k structures together fell under a suitable abstraction. Following Conal Elliott, we start from the fact that every Map k a can be regarded as a partial function k -> a determined by a list of pairs (k_i, a_i), and therefore as a total function k -> Maybe a. But Maybe a is a monad and k-> is a monad transformer to a Reader, so certainly “Map k“s are monads, right?

Well, in Haskell the default Monad instance for Maybe a requires a Monoid instance for a, so as long as we have that monoid instance then we are good. However, what if we wanted to combine two maps more like this?

Map k a -> Map k b -> Map k (a,b)

Here, pairing gives a monoidal product but not a Monoid object. Well, joining Maps seems an awful lot like having an Applicative, in the sense that the above looks like a monoidal product under f = Map k:

f a -> f b -> f (a,b)

Then I stumbled upon the documentation for Kmett’s semigroupoids library again, and remembered that he says that Map k is an Apply but not an Applicative. The typeclass Apply is just Applicative without “pure”, and when I went to look at the implementation it was just Map.intersectionWith id, where Map.intersectionWith has type

intersectionWith :: (a -> b -> c) -> Map k a -> Map k b -> Map k c

For normal people, this means that we are taking an inner join of the two maps and combining the values at the common keys with the supplied binary function. So what the hell does it mean to use the obviously non-binary function id?

Well, using id :: a -> a means that we have to unify a ~ b -> c, so we wind up with

intersectionWith id :: Map k (b -> c) -> Map k b -> Map k c

and at this point one can sort of look at the type, wave hands, and say that by parametricity the only useful thing this function could do is apply each (b -> c) to the corresponding b to get back a c. But how does the compiler know that this is the correct behavior?

The definition of intersectionWith is not that interesting: it takes the a -> b -> c and applies it to each a, and then each b. If a ~ b -> c, then id :: (b -> c) -> (b -> c), and the first step does nothing. The second step is the more interesting one: it applies the partally-evaluated function b -> c to b to yield the c as expected. Uncurrying this step gives uncurry ($): (b -> c, b) -> c, which is the same as uncurry id!

So id is a binary operation after all… mind blown. :-)