4

Is there any explanation for why a lifted function, when applied to 2-tuple, only applies to the 2nd entry:

f x = x + 1
f <$> (2,2)
    // -> (2,3)

On the other hand, tuples of any other length than 2 return errors. Also

:t f <$>

returns an error. Is it possible to see the type of f <$> when acting on tuples?

Is there any explanation for that behaviour?

The Data.Tuple documentation is extremely brief and has no mention of how functions are lifted to tuples. Is there any source explaining it?


Update. A part of question about 2-tuples is related to this answer, where, however, the above question about multiple length tuples is not addressed.

Community
  • 1
  • 1
Dmitri Zaitsev
  • 13,548
  • 11
  • 76
  • 110
  • On your side questions: (1) `:t (<$>) f` (prefix syntax) and `:t (f <$>)` (section syntax) would work. (2) With GHC 8, you can also turn on the `TypeApplications` extension and specialise `(<$>)` to pairs with `:t (<$>) @((,) _) f` (3) Going to [the definition of the class](https://hackage.haskell.org/package/base-4.9.0.0/docs/Data-Functor.html#t:Functor) and looking at the instances listed there can help with questions about (possibly existing) instances (in this case, you will find `Functor ((,) a)` but nothing for larger tuples). – duplode Dec 26 '16 at 07:06
  • @duplode Thanks, this works, but I still don't understand how larger tuples are different. Can't you apply the function to the last argument exactly the same way? – Dmitri Zaitsev Dec 26 '16 at 09:10

4 Answers4

3

One could (and arguably, GHC should) define a Functor instance for triples and larger tuples. To wit:

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

If this instance truly doesn't exist anywhere in base, I suspect that's mostly oversight, though I don't know the history well enough to say for sure. You can include this in any code where it seems useful, with the caveat that you should then absolutely put a fairly strict upper bound on the version of base in your *.cabal file, as this instance might reasonably be included in future versions of base. The PVP allows only the third component of the version to change in such a case, so include at least that many components in your upper bound!

Daniel Wagner
  • 145,880
  • 9
  • 220
  • 380
  • An oversight?? You mean they simply forgot to take care of tuples of length greater than 2?? – Dmitri Zaitsev Dec 26 '16 at 09:47
  • 1
    @DmitriZaitsev When you have built a system as big as GHC without forgetting any details, you can put a third question mark in each question. =) – Daniel Wagner Dec 26 '16 at 09:53
  • @DmitriZaitsev Creating new data types is cheap and [encouraged](http://stackoverflow.com/q/19072930/791604), so anonymous types like tuples and `Either`s are not as important as they might be in other languages. And again, I think it is unfair to use language like "introducing inconsistency", which sort of suggests malice on the part of the software authors, when the reality is much more likely "not noticing inconsistency", which is just the natural conclusion of a 25-year project that is the work of many authors from all across the globe. – Daniel Wagner Dec 26 '16 at 10:02
  • Can introducing and managing a new type be as simple and effortless and with less code than writing an anonymous triple, especially if you rarely use it? It is quite possible that there is a more substantial reason that we both don't know, rather than a plain oversight that indeed would be hard to imagine for this 25-year project. – Dmitri Zaitsev Dec 26 '16 at 10:33
  • 2
    @DmitriZaitsev Like Daniel Wagner, I also suspect it is an oversight which arose out of the relative rarity of use cases for the instances for larger tuples. Here is a [discussion about adding the instances](https://mail.haskell.org/pipermail/libraries/2016-January/026579.html) in the libraries mailing list earlier this year. Reading it shows that some people dislike the tuple instances because it is supposedly arbitrary and confusing that they only work with the last type parameter. (For what it's worth, I disagree: it is the only instance the language makes possible, so it isn't arbitrary.) – duplode Dec 26 '16 at 17:25
  • 2
    In my opinion, the oversight was making `n` tuples distinct types for every `n` - because of this, GHC technically has not ever conformed to the [Haskell standard](https://www.haskell.org/onlinereport/haskell2010/haskellch3.html#x8-360003.8) in its entirety - which explicitly says "Tuples are written (e1, …, ek), and may be of **arbitrary length** k ≥ 2". I suppose this design choice was made for performance reasons (a 10-tuple is one constructor instead of 9 constructors) - but the common wisdom is that the appearence of a large tuple in your program is a design error in and of itself. – user2407038 Dec 27 '16 at 00:08
  • @duplode Thanks, interesting read but disappointingly lengthy discussion seemingly not arriving to any decision. Couldn't see any convincing argument against allowing long tuples, there is obvious interest and use cases from some folks... I am literally buffled – Dmitri Zaitsev Dec 27 '16 at 10:41
  • 1
    @user2407038 IMO, _n_-tuple expressions `(a,b,c,...,ψ,ω)` should just be syntactic sugar for `(((...((a,b),c),...),ψ),ω)`, then we'd never need to worry about tuples with arity >2. Of course this would incur quite a bit of extra performance penalty unless tuples were made strict-unboxed by default (which might also be a good thing anyway). – leftaroundabout Dec 27 '16 at 17:12
  • 1
    Regardless, I'm ambivalent whether even the `(a,)` functor instance was ever a clever idea – arguably it would be better to just use `Writer` to get access to that functor, that would avoid any confusion as to “which element do we map over?” and “why is `length (10,20) ≡ 1`?”. – leftaroundabout Dec 27 '16 at 17:19
3

Is there any explanation for why a lifted function, when applied to 2-tuple, only applies to the 2nd entry

Because tuples are heterogeneous which means that, in general, it would not make sense to try to apply a function of type b -> c to each component of a tuple of type (a, b).

If you want pairs of values of the same type, you can declare your own type Pair and then have the functor instance apply the function to each component.

data Pair a = Pair { fst :: a
                   , snd :: a }

instance Functor Pair where
  fmap f (Pair fst snd) = Pair (f fst) (f snd)

Is it possible to see the type of f <$> when acting on tuples?

f <$> is a section (a partially applied infix operator). To get its type, you need to wrap it with parentheses like so:

:t (f <$>)

The Data.Tuple documentation is extremely brief and has no mention of how functions are lifted to tuples. Is there any source explaining it?

The combinator (<$>) (and (<*>)) are more general than just for tuples, you can find them in the Control.Applicative module.

gallais
  • 11,823
  • 2
  • 30
  • 63
  • [Product Identity Identity](https://hackage.haskell.org/package/base-4.9.0.0/docs/Data-Functor-Product.html) – Daniel Wagner Dec 26 '16 at 09:51
  • I understand that the combinators are more general, in which they pass to the individual lift implementation. My question here is more about the specific implementation for tuples (of any length), and I could not find any information about it when checking that link. – Dmitri Zaitsev Dec 26 '16 at 10:01
3

All the other answers here seem pretty good, but I don't think anyone precisely answered your question yet.

I believe the reason 2-tuples (and no other tuples) are treated this way by default is because this allows them to be used in the same way as a Writer in a monadic context. (That is, ((,) a) and Writer are isomorphic.)

For example, given a function running in a Writer monad:

import Control.Monad.Writer

foo :: Int -> Writer [String] Int
foo n = do tell ["Running foo " ++ show n]
           if (n <= 0) then do
             tell ["We are done!"]
             return 1
           else do
             rest <- foo (n-1)
             return (n * rest)

you can rewrite it using the Monad instance of ((,) a):

bar :: Int -> ([String], Int)
bar n = do tell' ["Running bar " ++ show n]
           if (n <= 0) then do
             tell' ["We are done!"]
             return 1
           else do
             rest <- bar (n-1)
             return (n * rest)
  where tell' str = (str, ())

and you'll find that these do the same thing:

runWriter (foo 5)
bar 5

up to the ordering of the pair.

The definition of tell' is only needed because ((,) a) hasn't been made an instance of MonadWriter for some reason.

(Edited to add:) While you could extend the definition to larger tuples, this doesn't really provide any additional generality over the definition for the pair: one component of the pair is a monoid to which you can write, and the other component is the underlying "value" in the monad context -- if you need more components for one or the other, you can just make the component a tuple itself.

K. A. Buhr
  • 45,621
  • 3
  • 45
  • 71
  • From some of the other discussion, including references to mailing list threads, it looks like maybe this wasn't the motivation. I just always assumed it was. – K. A. Buhr Dec 26 '16 at 21:59
  • Thank you for trying to find an explanation, but is the fact that 2-tuples are isomorphic to another monad where n-tuples for n>2 are not, a sufficient reason to justify making the general behaviour for n-tuples different from 2-tuples? – Dmitri Zaitsev Dec 27 '16 at 10:53
2

In this answer, I will just expand a bit on one of the suggestions I made in a comment.

Is it possible to see the type of f <$> when acting on tuples?

(<$>) is a polymorphic function:

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

With GHC 8, you can use the TypeApplications extension to specialise polymorphic functions by supplying instantiations of some or all of their type variables (in this case, f, a and b, in that order):

GHCi> :set -XTypeApplications 
GHCi> :t (<$>) @Maybe
(<$>) @Maybe :: (a -> b) -> Maybe a -> Maybe b
GHCi> :t (<$>) @Maybe @Int
(<$>) @Maybe @Int :: (Int -> b) -> Maybe Int -> Maybe b
GHCi> :t (<$>) @Maybe @_ @Bool
(<$>) @Maybe @_ @Bool :: (t -> Bool) -> Maybe t -> Maybe Bool
GHCi> :t (<$>) @_ @Int @Bool
(<$>) @_ @Int @Bool
  :: Functor t => (Int -> Bool) -> t Int -> t Bool
GHCi> :t (<$>) @Maybe @Int @Bool
(<$>) @Maybe @Int @Bool :: (Int -> Bool) -> Maybe Int -> Maybe Bool

To use that with pairs, use the prefix syntax for the pair type constructor:

GHCi> :t (<$>) @((,) _)
(<$>) @((,) _) :: (a -> b) -> (t, a) -> (t, b)
GHCi> -- You can use the specialised function normally.
GHCi> -- That includes passing arguments to it.
GHCi> f x = x + 1
GHCi> :t (<$>) @((,) _) f
(<$>) @((,) _) f :: Num b => (t, b) -> (t, b)

The _ in ((,) _) leaves it unspecified what the type of the first element of the pair (which is the first argument of the (,) type constructor) should be. Every choice of it gives rise to a different Functor. You can be more specific if you wish:

GHCi> :t (<$>) @((,) String) f
(<$>) @((,) String) f :: Num b => (String, b) -> (String, b)

Lastly, it is worth having a look at what happens if you try that with 3-tuples:

GHCi> :t (<$>) @((,,) _ _) f
(<$>) @((,,) _ _) f
  :: (Num b, Functor ((,,) t t1)) => (t, t1, b) -> (t, t1, b)

As Daniel Wagner discusses in his answer, base doesn't define a Functor instance for 3-tuples. In spite of that, the type checker cannot exclude the possibility that someone somewhere might have defined an instance specific for some choice of the first two type parameters, however pointless that would be. For that reason, the speculative constraint Functor ((,,) t t1) shows up in the type (no such thing happens with pairs because there is a Functor ((,) a) instance in base). As expected, that blows up as soon as we try to instantiate the first two type parameters:

GHCi> :t (<$>) @((,,) Bool String) f

<interactive>:1:1: error:
    • Could not deduce (Functor ((,,) Bool String))
        arising from a use of ‘<$>’
      from the context: Num b
        bound by the inferred type of
                 it :: Num b => (Bool, String, b) -> (Bool, String, b)
        at <interactive>:1:1
    • In the expression: (<$>) @((,,) Bool String) f
Community
  • 1
  • 1
duplode
  • 33,731
  • 7
  • 79
  • 150
  • "no such thing happens with pairs because there is a Functor ((,) a) instance in base)" - now I am losing the line, aren't we looking for why the base only treated 2-tuples in first place? – Dmitri Zaitsev Dec 27 '16 at 10:49
  • @DmitriZaitsev What I meant is that, since pairs do have a `Functor` instance, the type of `(<$>) @((,) _)` requires no extra constraints to demand that `((,) a)` is a functor. For a somewhat more implausible example of "speculative constraint" in the sense I used above, try `:t negate []` in GHCi. – duplode Dec 27 '16 at 16:33
  • Thank you for clarifying, but I still feel this shows that "F(x) behaves differently from F(y) because x behaves different from y". That is, in your example, the interpretation for triples is (inconsistently) different than pairs, which causes the (messy?) differences further down the line. Instead, wouldn't having a consistent behaviour for all tuples remove this problem? – Dmitri Zaitsev Dec 28 '16 at 02:15
  • @DmitriZaitsev It is indeed inconsistent. My answer is merely describing the *status quo* -- I believe it would be a good thing to have instances for *n*-tuples up to a sufficiently large *n*. (Cf. user2407038 and leftaroundabout's comments to Daniel Wagner's answer for a more radical interpretation of "consistent behaviour for all tuples".) – duplode Dec 28 '16 at 02:37