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

2

8

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?

Attu answered 9/7, 2014 at 14:20 Comment(6)
You may want to look on this: programmers.stackexchange.com/questions/185585/…Calycine
@Calycine are you recommending always using tuples unless you need Haskell's automatic curreying/uncurrying?Attu
My opinion is that tuples should be used when the two values are inherently connected, such as a tuple representing coordinates (x, y). Luckily for 2-tuples, we have curry and uncurry to convert between these representations for whenever one is more convenient than the other.Illusionism
Another great use for tuples is when you don't want to make a custom data type, but would still like a synonym to keep type signatures clear: type FName = String; type LName = String; type Age = Int; type Person = (FName, LName, Age); greet :: Person -> IO (); greet (fname, lname, age) = putStrLn $ if age >= 30 then "Hello, " ++ fname ++ " " ++ lname else "Sup?"Illusionism
@chrisdew It depends upon the type of the problem. As bhekilir suggested use tuples if the values are connected. Most of the time I don't use tuples. I make the tuples structure into a Record if they are inherently connected. Currying allows easier reuse, so I would stick up with that generally.Calycine
@chrisdew In your example, I personally would choose to separate the arguments to collide, since it's not unlikely that you could have multiple collisions with the same object, e.g. data Meteor = Meteor; instance Collide Planet Meteor where ... and meteorStorm :: [Meteor] -> [String]; meteorStorm = map (flip collide Planet), whereas if you use tuples you'd have to write it meteorStorm = map (\m -> collide (m, Planet). While about the same number of characters, I personally think the former is more readable.Illusionism
B
6

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
Bloxberg answered 9/7, 2014 at 15:30 Comment(0)
M
2

I would say, normally, function should be curried (so no tuples) except is the arguments themself are tuples. For example If you write a function to add two numbers, you have 2 arguments, so you should write it :

add :: Num a => a -> a -> a
add x y = x + y

Now, if for some reason your are using 2-uple as 2-D point, and you want to add two points. That's till two arguments, which appears to be tuples so you would write like that

add :: Num a => (a,a) -> (a,a) -> (a,a)
add (x,y) (x,y') = (x+x', y+y')

Writing

add :: Num a => a -> a -> a -> a -> (a,a)
add a b c d = (a+c, b+d)

Wouldn't really make sense because the entity your dealing with are tuples. You wouldn't write that way either

add :: Num a => ((a,a),(a,a)) -> (a,a)

In our example, it's probable that the collide function is always called in a context when thing to check are served as a tuple (because maybe there is stage which collect all possible collision resulting in a list of 2-uples). In that situation it might be easier then to have a uncurried function, so you can map collide over it.

Maighdiln answered 9/7, 2014 at 17:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.