7

I have just started learning Haskell. As I know maximum gives the max value for a list of integers. So, maximum [2,5,7,1] gives 7. But why by giving a tuple input does maximum always give the second element? E.g., maximum (8,1) gives 1. The same thing happens with sum (8,1), product (5,2), minimum (4,5)... all give the second element of the tuple. So, can anyone explain to a beginner why this happens?

Supriyo Halder
  • 200
  • 1
  • 7
  • 2
    For some additional context [Why does length return 1 for a tuple with 2 elements, and gives an error for a tuple with more elements?](https://stackoverflow.com/questions/36460833/why-does-length-return-1-for-a-tuple-with-2-elements-and-gives-an-error-for-a-t) – Redu Nov 06 '19 at 10:47
  • 2
    A key concern is that for __tuples__, the types of the 2 elements are allowed to differ, so that (8, True) and (True, 8) are perfectly legal Haskell expressions. So there is no general way to define a maximum function that is both general and intuitively satisfying. For __lists__, all elements have to be of the same type, so [True, 8] is illegal, while maximum [8,1] and maximum [1,8] are both legal and evaluate to 8, as you would intuitively expect. – jpmarinier Nov 06 '19 at 11:31
  • 4
    Tuples aren't sequences in the sense that lists are. They are single values with multi-values tags. `(8, 1)` can be thought of as `1` with a tag of `8`; `(1,2,3,4,5)` can be thought of as a `5` with a four distinguished tags `1`, `2`, `3`, and `4`. – chepner Nov 06 '19 at 12:57

2 Answers2

8

Short answer: for a 2-tuple, the Foldable instance takes (only) the second item into account. The maximum function will thus always return the second item of the 2-tuple.

Because a 2-tuple is an instance of Foldable. Indeed, it is defined as [src]:

instance Foldable ((,) a) where
    foldMap f (_, y) = f y

    foldr f z (_, y) = f y z

The maximum is in essence a foldr pattern. It is implemented as an equivalent of:

maximum = foldr1 max

Where foldr1 is implemented as:

foldr1 f = fromMaybe (error "…") . foldr mf Nothing
    where mf x m = Just (case m of
                             Nothing -> x
                             Just y  -> f x y)

So that means that for a 2-tuple, it will be implemented as:

maximum (_, y) = fromMaybe (error "…") (mf y Nothing)
    where mf x m = Just (case m of
                             Nothing -> x
                             Just y  -> max x y)

So here we call mf with y (as x parameter), and Nothing as m. The case … of … this matches with Nothing and returns x. So the maximum for a 2-tuple is defined as:

maximum (_, y) = fromMaybe (error "…") (Just y)

and thus shorter:

-- maximum for a (a, b)
maximum (_, y) = y
Willem Van Onsem
  • 443,496
  • 30
  • 428
  • 555
  • Do you know why it's defined this way rather than taking both values into account? – TheInnerLight Nov 06 '19 at 10:46
  • 1
    @TheInnerLight: it can not take both values into account since the instance of a `Foldable` should be of kind `* -> *`, so an `instance Foldable (,)` would "crash", since that has as kind `* -> * -> *`. – Willem Van Onsem Nov 06 '19 at 10:48
  • 1
    @TheInnerLight what would be the maximum of this `('qwerty', 12345)`? Define the maximum of any tuple is actually senseless. But you can always define your own `maxTuple` function for tuples of type `(Int, Int)` – lsmor Nov 06 '19 at 10:50
  • 3
    @TheInnerLight Because the types are different, in general. For instance `maximum ("hello", 4)` can not consider both elements. Here the type is `(,) String Int`, which is seen by `Foldable` as a container-like type `(,) String` which happens to have `Int` as its element. I'm not terribly convinced it was a good idea to allow `Foldable ((,) a)` in the libraries, but if we allow that, we have no type-safe option but to consider only the second component of the pairs. – chi Nov 06 '19 at 10:51
  • 1
    @chi: me neither. TO be honest, I think a 2-tuple should probably not be a `Foldable`, since it creates a lot of confusion. Yes, we can see a 2-tuple as some sort of "value with a context" (that context is then the first item), but it will likely give a lot of confusion. – Willem Van Onsem Nov 06 '19 at 10:58
  • 2
    To play devil's advocate, I think the `Foldable` instance is good not because you would often want to fold a single-element container, but because it emphasizes the fact that a tuple is not just a minor variation on a list. The fact that `maximum (8,1)` doesn't return `8` should force you to stop and ask yourself why you have a tuple in the first place. – chepner Nov 06 '19 at 13:35
  • 1
    I'm not sure where that metaphor is going. My point is, if you think passing a tuple to `maximum` is a good idea, rethink why you are using a tuple. Tuples are foldable, just not in the way lists are foldable, and that's neither good nor bad, just *different*. – chepner Nov 06 '19 at 15:22
  • @chepner If the goal is to discourage me from using `maximum (x,y)`, a type error would be the most effective means, IMO. Unrelated: I wonder how many packages on hackage rely on such a weird instance, after it being in `base` for 5? years. – chi Nov 06 '19 at 15:51
  • 1
    My goal *isn't* to discourage you from using `maximum (x,y)`; it's to discourage you from using `(x, y)` in a context where you think comparing `x` and `y` to each other will be necessary. – chepner Nov 06 '19 at 16:05
1

Tuples in Haskell are not multi-valued containers like they are in, say, Python. Rather, they are closer to single-valued containers like Maybe a or Either b a: a value with a context. While both Maybe and Either carry the concept of possibly no value (Either simply providing more information than Maybe about the lack of a value), a tuple carries the context of information about the value itself.

A value like (8, 1) does not contain two values 8 and 1. Rather, 8 is part of a container that contains a 1.

As such, tuples are foldable, even if doing so seems trivial. Reducing a value of type (a, b) to a value of b simply has to return the value of type b, without worrying about what to do with the other values of type b, because there aren't any.

>>> maximum (Just 5)
5
>>> minimum (Just 5)
5

>>> maximum (Right 5)
5
>>> minimum (Right 5)
5

>>> maximum (True, 5)
5
>>> minimum (True, 5)
5

Such functions are total with tuples, though, as compared to Maybe or Either:

>>> maximum Nothing
*** Exception: maximum: empty structure
>>> maximum (Left 5)
*** Exception: maximum: empty structure

No matter how many types the tuple contains, all but the right-most one will fixed for any given instance of Foldable.

-- Actual instance for (a, b)
instance Foldable ((,) a) where
    foldMap f (_, y) = f y
    foldr f z (_, y) = f y z


-- Instance for (a, b, c) if you wanted it
instance Foldable ((,,) a b) where
    foldMap f (_, _, y) = f y
    foldr f z (_, _, y) = f y z
chepner
  • 497,756
  • 71
  • 530
  • 681