Когда функции Haskell должны принимать кортежи, а не несколько аргументов?

В http://www.haskell.org/pipermail/haskell-cafe/2007-August/030096.html метод класса типов collide определяется как принимающий 2-кортеж в качестве единственного аргумента, а не два «обычных» аргумента (я думаю, что понимаю частичное применение и т. д.).

{-# 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"

Какова цель этого?

Когда лучше использовать аргумент кортежа, а не несколько аргументов?


person fadedbee    schedule 09.07.2014    source источник
comment
Вы можете посмотреть это: программисты.stackexchange.com/questions/185585/   -  person Sibi    schedule 09.07.2014
comment
@Sibi Вы рекомендуете всегда использовать кортежи, если только вам не нужна автоматическая обработка/отмена обработки в Haskell?   -  person fadedbee    schedule 09.07.2014
comment
Мое мнение заключается в том, что кортежи следует использовать, когда два значения неразрывно связаны, например кортеж, представляющий координаты (x, y). К счастью для двойных кортежей, у нас есть curry и uncurry для преобразования между этими представлениями, когда одно удобнее другого.   -  person bheklilr    schedule 09.07.2014
comment
Еще одно полезное применение кортежей — это когда вы не хотите создавать пользовательский тип данных, но все же хотите использовать синоним, чтобы сигнатуры типов оставались четкими: 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?"   -  person bheklilr    schedule 09.07.2014
comment
@chrisdew Это зависит от типа проблемы. Как предложил bhekilir, используйте кортежи, если значения связаны. В большинстве случаев я не использую кортежи. Я превращаю кортежи в запись, если они по своей сути связаны. Каррирование упрощает повторное использование, поэтому я бы придерживался этого в целом.   -  person Sibi    schedule 09.07.2014
comment
@chrisdew В вашем примере я лично предпочел бы разделить аргументы на collide, поскольку вполне вероятно, что у вас может быть несколько столкновений с одним и тем же объектом, например. data Meteor = Meteor; instance Collide Planet Meteor where ... и meteorStorm :: [Meteor] -> [String]; meteorStorm = map (flip collide Planet), тогда как, если вы используете кортежи, вам придется писать meteorStorm = map (\m -> collide (m, Planet). Хотя количество символов примерно такое же, я лично считаю, что первое более читабельно.   -  person bheklilr    schedule 09.07.2014


Ответы (2)


Я почти никогда не пишу функции, которые принимают кортежи в качестве аргументов. Если возникает ситуация, когда переменные связаны по своей сути (как упоминалось в комментарии bheklilr), я, скорее всего, упакую это в свой собственный отдельный тип данных и сопоставление с шаблоном.

Одной из распространенных ситуаций, когда вы можете захотеть определить функцию, которая принимает кортежи в качестве аргументов, является ситуация, когда у вас есть список (или любое произвольное Functor) кортежей, которые вы создаете на лету, но вы хотите отобразить его. с некоторой функцией, например.

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

Вы можете захотеть, скажем, добавить первое и второе значения всех кортежей в вашей сетке (по любой причине), что вы можете сделать, сопоставив функцию, потребляющую кортежи, с grid:

addTuple :: (Int, Int) -> Int
addTuple (x, y) = x + y

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

Что я бы предпочел сделать в этой ситуации, так это просто использовать uncurry (:: (a -> b -> c) -> (a, b) -> c) для использования +, как обычно:

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

Это, возможно, яснее и определенно короче; также очень легко определить аналоги более высокого порядка, такие как uncurry3, например:

> let uncurry3 f (a, b, c) = f a b c
> uncurry3 (\a b c -> a + b + c) (1, 2, 3)
6
person Benjamin Kovach    schedule 09.07.2014

Я бы сказал, что обычно функция должна быть каррирована (поэтому никаких кортежей), за исключением того, что сами аргументы являются кортежами. Например, если вы пишете функцию для сложения двух чисел, у вас есть 2 аргумента, поэтому вы должны написать ее:

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

Теперь, если по какой-то причине вы используете 2-uple в качестве точки 2-D, и вы хотите добавить две точки. Это до двух аргументов, которые выглядят как кортежи, поэтому вы должны написать так

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

Пишу

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

На самом деле это не имеет смысла, потому что сущность, с которой вы имеете дело, — это кортежи. Вы бы тоже так не писали

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

В нашем примере, вероятно, функция collide всегда вызывается в контексте, когда объект для проверки обслуживается как кортеж (потому что, возможно, существует этап, который собирает все возможные коллизии, приводящие к списку 2- вверх). В этой ситуации может быть проще иметь функцию без карри, поэтому вы можете отобразить collide поверх нее.

person mb14    schedule 09.07.2014