Why does the 2-tuple Functor instance only apply the function to the second element?
Asked Answered
M

3

43
import Control.Applicative

main = print $ fmap (*2) (1,2)

produces (1,4). I would expect it it to produce (2,4) but instead the function is applied only to the second element of the tuple.

Update I've basically figured this out almost straight away. I'll post my own answer in a minute..

Marsden answered 18/11, 2012 at 17:22 Comment(0)
M
19

The Functor instance is actually from the GHC.Base module which is imported by Control.Applicative.

Trying to write the instance I want, I can see that it won't work, given the definition of tuples; the instance requires just one type parameter, while the 2-tuple has two.

A valid Functor instance would at least have to be on tuples, (a,a) that have the same type for each element, but you cannot do anything sneaky, like define the instance on:

 type T2 a = (a,a)

because instance types aren't permitted to be synonyms.

The above restricted 2-tuple synonym is logically the same as the type:

data T2 a = T2 a a

which can have a Functor instance:

instance Functor T2 where
    fmap f (T2 x y) = T2 (f x) (f y)

As Gabriel remarked in the comments, this can be useful for branching structures or concurrency.

Marsden answered 18/11, 2012 at 17:35 Comment(4)
Actually, that IS useful as an instance of the functor class. For example, I can define a tree as type Tree a = Free T2 a. In fact, most uses of that type (in its capacity as a functor) involve branching or concurrency of some sort.Infantine
It's worth mentioning that if you want to specify which part of the tuple to map over, you can use Control.Lens. A Setter is like a Functor instance that you specify explicitly, so over _1 (+1) (5,3) ==> (6,3); over _2 (*2) ("funny,2) ==> ("funny",4); over both length ("hi","there") ==> (2,5); over (both._1) (*10) ((1,2),(3,4)) ==> ((10,2),(30,4)).Halfpenny
You can also use arrows if you want to run it on the first element or second element: first and second.Plunge
@CMCDragonkai, I would much prefer the simpler Bifunctor versions of those functions in this case; it's easier to understand and generalizes in what I think is probably a more common direction.Escuage
E
33

Let me answer this with a question: Which output do you expect for:

main = print $ fmap (*2) ("funny",2)

You can have something as you want (using data Pair a = Pair a a or so), but as (,) may have different types in their first and second argument, you are out of luck.

Emmyemmye answered 18/11, 2012 at 17:59 Comment(0)
E
27

Pairs are, essentially, defined like this:

data (,) a b = (,) a b

The Functor class looks like this:

class Functor f where
  fmap :: (a -> b) -> f a -> f b

Since the types of function arguments and results must have kind * (i.e. they represent values rather than type functions that can be applied further or more exotic things), we must have a :: *, b :: *, and, most importantly for our purposes, f :: * -> *. Since (,) has kind * -> * -> *, it must be applied to a type of kind * to obtain a type suitable to be a Functor. Thus

instance Functor ((,) x) where
  -- fmap :: (a -> b) -> (x,a) -> (x,b)

So there's actually no way to write a Functor instance doing anything else.


One useful class that offers more ways to work with pairs is Bifunctor, from Data.Bifunctor.

class Bifunctor f where
  bimap :: (a -> b) -> (c -> d) -> f a c -> f b d
  bimap f g = first f . second g

  first :: (a -> b) -> f a y -> f b y
  first f = bimap f id

  second :: (c -> d) -> f x c -> f x d
  second g = bimap id g

This lets you write things like the following (from Data.Bifunctor.Join):

  newtype Join p a =
    Join { runJoin :: p a a }

  instance Bifunctor p => Functor (Join p) where
    fmap f = Join . bimap f f . runJoin

Join (,) is then essentially the same as Pair, where

data Pair a = Pair a a

Of course, you can also just use the Bifunctor instance to work with pairs directly.

Escuage answered 5/1, 2016 at 4:38 Comment(6)
Why is -- fmap :: (a -> b) -> (y,x,a) -> (y,x,b) not working for 3-tuples?Vinegar
@DmitriZaitsev It would, if you defined a Functor instance for ((,,) x y). (x,y) and (x,y,z) are very different (families of) types, as there is no single "tuple" type (family).Misapprehend
"So there's actually no way to write a Functor instance doing anything else." What about fmap :: (a -> b) -> (a,x) -> (b,x)?Alejandraalejandrina
@MateenUlhaq, that's just not the type fmap has to have according to the definition of the Functor class.Escuage
Ah, I see. Theoretically, could one define instance Functor ((*, x))? (Not sure what the correct notation is for an unapplied type, so I used *.)Alejandraalejandrina
@MateenUlhaq, it's a bit hard to guess what you might mean, but one type that's occasionally useful is newtype Flip f a b = Flip { unFlip :: f b a }.Escuage
M
19

The Functor instance is actually from the GHC.Base module which is imported by Control.Applicative.

Trying to write the instance I want, I can see that it won't work, given the definition of tuples; the instance requires just one type parameter, while the 2-tuple has two.

A valid Functor instance would at least have to be on tuples, (a,a) that have the same type for each element, but you cannot do anything sneaky, like define the instance on:

 type T2 a = (a,a)

because instance types aren't permitted to be synonyms.

The above restricted 2-tuple synonym is logically the same as the type:

data T2 a = T2 a a

which can have a Functor instance:

instance Functor T2 where
    fmap f (T2 x y) = T2 (f x) (f y)

As Gabriel remarked in the comments, this can be useful for branching structures or concurrency.

Marsden answered 18/11, 2012 at 17:35 Comment(4)
Actually, that IS useful as an instance of the functor class. For example, I can define a tree as type Tree a = Free T2 a. In fact, most uses of that type (in its capacity as a functor) involve branching or concurrency of some sort.Infantine
It's worth mentioning that if you want to specify which part of the tuple to map over, you can use Control.Lens. A Setter is like a Functor instance that you specify explicitly, so over _1 (+1) (5,3) ==> (6,3); over _2 (*2) ("funny,2) ==> ("funny",4); over both length ("hi","there") ==> (2,5); over (both._1) (*10) ((1,2),(3,4)) ==> ((10,2),(30,4)).Halfpenny
You can also use arrows if you want to run it on the first element or second element: first and second.Plunge
@CMCDragonkai, I would much prefer the simpler Bifunctor versions of those functions in this case; it's easier to understand and generalizes in what I think is probably a more common direction.Escuage

© 2022 - 2024 — McMap. All rights reserved.