A curious associativity of the <$> operator

Published on

The <$> operator in Haskell is an infix synonym for fmap and has the type

(<$>) :: Functor f => (a -> b) -> f a -> f b

For example:

> negate <$> Just 5
Just (-5)

Like all other operators, <$> has a fixity, which can be infixl, infixr, or infix. The fixity defines how an expression like

abs <$> negate <$> Just 5

is parsed.

If <$> were defined as infix, then the above expression wouldn’t parse at all, giving an error like

Precedence parsing error
    cannot mix ‘<$>’ [infix 4] and ‘<$>’ [infix 4] in the same infix expression

If <$> instead were defined as infixr, then the above expression would parse as

abs <$> (negate <$> Just 5)

meaning first apply negate to 5 inside Just, and then go inside Just once more and apply abs.

However, <$> is defined as infixl, so that the applicative chains like

(+) <$> Just 5 <*> Just 6

would parse correctly. This means that

abs <$> negate <$> Just 5

is parsed as

(abs <$> negate) <$> Just 5

which is probably not what you meant. Or is it?

It turns out that if you paste that expression (either parenthesized or not) into ghci, you’ll get back Just 5 in all cases.

This happens because of the following instance defined in the base library:

instance Functor ((->) r) where
  fmap = (.)

(Note that (->) r is essentially the Reader monad, so it’s not surprising that it’s a functor.)

So the expression

(abs <$> negate) <$> Just 5

does make sense and is equivalent to

(abs . negate) <$> Just 5

The operators * for which a * (b * c) = (a * b) * c are called associative. Usually associative operators have the type a -> a -> a (either polymorphic of for a specific a), so that both ways to put parentheses typecheck.

The <$> operator is curious because it doesn’t have such a type and yet is associative:

f <$> (g <$> h) =
{- the definition of <$> -}
fmap f (fmap g h) =
{- the definition of (.) -}
(fmap f . fmap g) h =
{- the functor law -}
fmap (f . g) h =
{- the definition of <$> -}
(f . g) <$> h =
{- the Functor instance for (->) r -}
{- g has to be a function for the top expression to typecheck -}
(f <$> g) <$> h