Search code examples
haskelltuplestypeclassmultimethod

When should Haskell functions take tuples, rather than multiple arguments?


In http://www.haskell.org/pipermail/haskell-cafe/2007-August/030096.html the typeclass method collide is defined as taking a 2-tuple as its single argument, rather than two "normal" arguments (I think I understand partial application, etc.).

{-# OPTIONS_GHC -fglasgow-exts
        -fallow-undecidable-instances
        -fallow-overlapping-instances #-}

module Collide where

class Collide a b where
    collide :: (a,b) -> String

data Solid = Solid
data Asteroid = Asteroid
data Planet = Planet
data Jupiter = Jupiter
data Earth = Earth

instance Collide Asteroid Planet where
    collide (Asteroid, Planet) = "an asteroid hit a planet"

instance Collide Asteroid Earth where
    collide (Asteroid, Earth) = "the end of the dinos"

-- Needs overlapping and undecidable instances
instance Collide a b => Collide b a where
    collide (a,b) = collide (b, a)

-- ghci output
*Collide> collide (Asteroid, Earth)
"the end of the dinos"
*Collide> collide (Earth, Asteroid)
"the end of the dinos"

What is the purpose of this?

When is it better to use a tuple argument rather than multiple arguments?


Solution

  • I almost never write functions that take tuples as arguments. If the situation arises where variables are inherently connected (as bheklilr mentioned in a comment), I'm more likely to box that up into it's own separate data type and pattern match on it.

    One common situation where you might want to define a function that takes tuples as arguments is when you have a list (or any arbitrary Functor) of tuples that you generate on the fly, but want to map over it with some function, e.g.

    grid :: [(Int, Int)]
    grid = (,) <$> [1..10] <*> [1..10]
    

    You might want to, say, add the first and second values of all of the tuples in your grid (for whatever reason), which you could do by mapping a tuple-consuming function over grid:

    addTuple :: (Int, Int) -> Int
    addTuple (x, y) = x + y
    
    sumPoints :: [(Int, Int)] -> [Int]
    sumPoints = map addTuple
    

    What I would rather do in this situation is just use uncurry (:: (a -> b -> c) -> (a, b) -> c) to use + just like normal:

    sumPoints :: [(Int, Int)] -> [Int]
    sumPoints = map (uncurry (+))
    

    This is arguably clearer and definitely shorter; it's also extremely easy to define higher-order analogues such as uncurry3, for example:

    > let uncurry3 f (a, b, c) = f a b c
    > uncurry3 (\a b c -> a + b + c) (1, 2, 3)
    6