Search code examples
haskelltuplesfunctor

Why is the Functor instance for (,) mapping on the second value?


In Haskell, the Functor instance of (,) is apparently

instance Functor (,) where
    fmap f (a,b) = (a,f b)

This results in the unintuitive fact that:

> fmap (const 5) [1, 2]
[5,5]
> fmap (const 5) (1, 2)
(1,5)

Now, using this definiton would work much better, in my opinion:

instance Functor (,) where
    fmap f (a,b) = (f a,f b)

It would work like this:

> fmap (const 5) (1, 2)
(5,5)

Why is it not like this?


Solution

  • The instance is actually not instance Functor (,) like you said. That wouldn't be well-kinded:

    Prelude> :k Functor
    Functor :: (* -> *) -> Constraint
    Prelude> :k (,)
    (,) :: * -> * -> *
    

    i.e., (,) takes two type arguments (the types of both tuple-fields) to construct a tuple-type, but the Functor class is actually for type-constructors that take only one argument, like

    Prelude> :k []
    [] :: * -> *
    Prelude> :k IO
    IO :: * -> *
    

    So why is there a functor instance at all? Frankly, I think this is one of the instances which should not have been defined, precisely because it's confusing that tuples aren't symmetric. However, it can actually be defined and there's only one way to do it, so this choice by the standard libraries is certainly not unreasonable. The trick is currying: you can partially apply the (,) constructor to any fixed left-field type, and that gives you then a single-argument type constructor. For example

    Prelude> :k (,) Int
    (,) Int :: * -> *
    

    so you can for instance have

    instance Functor ((,) Int)
    

    Clearly it doesn't depend on what concrete type the left field has, so you can as well make it

    instance Functor ((,) a)
    

    In value-level Haskell, this section would be written

    instance Functor (a,)
    

    Unlike at the term level, partial application does only work for the leftmost argument (the section (,b) would actually be be sugar for \a -> (a,b), but there are no type-level lambdas in Haskell), so instance Functor ((,) a) is the only instance that's possible here.

    To get the behaviour you asked for, i.e. function applied to both/all fields, you need both fields to actually have the same type. I.e., you need a type constructor that has only one argument to begin with, and just uses that type twice for the fields of its value constructor. A standard type with that behaviour is V2.