Search code examples
haskelltuplesfunctor

Why fmap doesn't work for a tuple?


In the below, I tried to use fmap on a tuple, but this didn't work, although it worked for a list and Just 4:

Prelude> fmap (+3) (Just 4)
Just 7
Prelude> fmap (+3) [1,2,3,4]
[4,5,6,7]
Prelude> fmap (+3) (10,11,12,13,14)

<interactive>:38:1: error:
    * Non type-variable argument
        in the constraint: Functor ((,,,,) a b1 c d)
      (Use FlexibleContexts to permit this)
    * When checking the inferred type
        it :: forall a b1 c d b2.
              (Num d, Num c, Num b1, Num a, Num b2, Functor ((,,,,) a b1 c d)) =>
              (a, b1, c, d, b2)
Prelude>

Solution

  • Why fmap doesn't work for a [5-]tuple?

    Because no one has added the 5-tuple Functor instance to base yet. If you have a look at the list of Functor instances provided by base, you will find Functor ((,) a), the instance for pairs, but not the instances for larger tuples, including Functor ((,,,,) a b c d), which is what you'd need here.

    The follow up question would of course be: Why has no one added the 5-tuple Functor instance to base yet? One reason has to do with necessity (or the lack thereof): in practice, pairs show up much more often than larger tuples, and as tuples get larger it becomes harder and harder to justify using them instead of a non-anonymous type suited to one's use case. That being so, the demand for Functor instances for larger tuples isn't all that big.

    Though you didn't mention which behaviour you expected for Functor ((,,,,) a b c d), it is worth noting that fmap for pairs acts only on the second component, and instances for larger tuples would analogously only deal with the last component.

    GHCi> fmap not (False, True)
    (False,False)
    

    Two reasons for that are:

    1. The types of the components can be different, and so there is no way e.g. fmap not ("foobar", True) could possibly change both components.

    2. There is no way to flip type constructors when writing instances, and so e.g. there can't be a Functor instance for pairs that acts on the first component (unless you use a newtype wrapper, but that is besides the point).

    While this behaviour might seem surprising, it is entirely reasonable if you think of a pair with type (a, b) as a b value with an annotation (tag, label, extra stuff -- however you like to call it) of type a attached to it. In the cases you'd rather think of it as a pair of two values that can be independently modified, you can resort to the Bifunctor class:

    GHCi> import Data.Bifunctor
    GHCi> first reverse ("foobar", True)
    ("raboof",True)
    GHCi> second not ("foobar", True)
    ("foobar",False)
    GHCi> bimap reverse not ("foobar", True)
    ("raboof",False)
    

    (base doesn't provide Trifunctor, Tetrafunctor, and so forth due to lack of need, as discussed at the beginning.)

    It is just as reasonable to regard larger tuples in the same manner when it comes to giving them Functor instances; and in fact those instances arguably should exist for the sake of consistency. However, some people do strongly dislike the instance for pairs, which has led proposals to add the instances for other tuples to stall.

    P.S.: It is perhaps worth mentioning that one of the (many) use cases of the lens library is using things that aren't Functors as functors. That give us a convenient way to see what Functor and (if that were a thing) Pentafunctor instances would do with a 5-tuple:

    GHCi> import Control.Lens
    GHCi> over _5 (+3) (10,11,12,13,14)
    (10,11,12,13,17)
    GHCi> over _4 (+3) (10,11,12,13,14)
    (10,11,12,16,14)
    GHCi> over _3 (+3) (10,11,12,13,14)
    (10,11,15,13,14)
    GHCi> over _2 (+3) (10,11,12,13,14)
    (10,14,12,13,14)
    GHCi> over _1 (+3) (10,11,12,13,14)
    (13,11,12,13,14)
    

    There are even ways to map over all components...

    GHCi> over both (+3) (13,14)
    (16,17)
    GHCi> over each (+3) (10,11,12,13,14)
    (13,14,15,16,17)
    

    ... though, unsurprisingly, they demand all components to have the same type:

    GHCi> over each (+3) (True,11,12,13,14)
    
    <interactive>:9:12: error:
        * No instance for (Num Bool) arising from a use of `+'
        * In the second argument of `over', namely `(+ 3)'
          In the expression: over each (+ 3) (True, 11, 12, 13, 14)
          In an equation for `it':
              it = over each (+ 3) (True, 11, 12, 13, 14)
    
    GHCi> :set -XPartialTypeSignatures
    GHCi> :set -fno-warn-partial-type-signatures
    GHCi> :t \f -> over each f :: (_,_,_,_,_) -> _
    \f -> over each f :: (_,_,_,_,_) -> _
      :: (w -> b5) -> (w, w, w, w, w) -> (b5, b5, b5, b5, b5)