【问题标题】:When should Haskell functions take tuples, rather than multiple arguments?Haskell 函数什么时候应该采用元组,而不是多个参数?
【发布时间】:2014-07-09 14:20:43
【问题描述】:

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"

这样做的目的是什么?

什么时候使用元组参数而不是多个参数更好?

【问题讨论】:

  • 我的意见是当两个值有内在联系时应该使用元组,例如表示坐标(x, y)的元组。幸运的是,对于 2 元组,我们有 curryuncurry 来在这些表示之间进行转换,以便在其中一个比另一个更方便时进行转换。
  • 元组的另一个重要用途是当您不想创建自定义数据类型,但仍希望使用同义词来保持类型签名清晰: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?"
  • @chrisdew 这取决于问题的类型。正如 bhekilir 建议的那样,如果值是连接的,则使用元组。大多数时候我不使用元组。如果它们本质上是连接的,我会将元组结构变成一个记录。柯里化可以更容易地重用,所以我通常会坚持这一点。
  • @chrisdew 在您的示例中,我个人会选择将参数分隔到collide,因为您不太可能与同一个对象发生多次冲突,例如data Meteor = Meteor; instance Collide Planet Meteor where ...meteorStorm :: [Meteor] -> [String]; meteorStorm = map (flip collide Planet),而如果您使用元组,则必须编写 meteorStorm = map (\m -> collide (m, Planet)。虽然字符数量大致相同,但我个人认为前者更具可读性。

标签: haskell tuples typeclass multimethod


【解决方案1】:

我几乎从不编写将元组作为参数的函数。如果出现变量固有连接的情况(如评论中提到的 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 -&gt; b -&gt; c) -&gt; (a, b) -&gt; 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

【讨论】:

    【解决方案2】:

    我会说,通常情况下,函数应该被柯里化(所以没有元组),除了参数本身是元组。例如,如果你写一个函数来添加两个数字,你有 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)
    

    真的没有意义,因为您处理的 entity 是元组。 你也不会这样写

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

    在我们的示例中,collide 函数很可能总是在上下文中调用,当要检查的东西服务作为一个元组(因为可能有一个阶段收集所有可能的冲突导致2-uples 的列表)。在这种情况下,使用 uncurried 函数可能更容易,因此您可以在其上映射 collide

    【讨论】:

      猜你喜欢
      • 2010-12-11
      • 1970-01-01
      • 2021-12-06
      • 2020-07-16
      • 2012-06-07
      • 1970-01-01
      • 2018-05-12
      • 1970-01-01
      • 2021-05-03
      相关资源
      最近更新 更多