Haskell:将参数组合成元组而不是使用不同的参数有什么含义?

dav*_*idA 2 parameters haskell functional-programming tuples interface

作为 Haskell 初学者,我对最佳实践很好奇。特别是,在没有其他要求的情况下,是使用元组关联相关的函数参数,还是保持“裸”更好?

例如

vector :: Float -> Float -> Float -> Vector
Run Code Online (Sandbox Code Playgroud)

对比

vector :: (Float, Float, Float) -> Vector
Run Code Online (Sandbox Code Playgroud)

我问的原因是有时参数的某些方面(例如 2D 或 3D 点或向量中的 x 坐标)通常与其他参数(例如 y 和 z 坐标)相关联。我可以看到在这两种情况下如何使用模式匹配,但我很想知道使用元组或不同参数是否有严重的影响。

当涉及到其他参数时,元组的使用似乎清楚地表明某组参数是相互关联的。但是当函数只将元组作为参数时,它也会使代码更加冗长。

lef*_*out 8

根据经验,我建议永远不要将元组放在函数签名的参数中。

为什么?好吧,如果重点是将东西组合在一起,那么元组在这方面做得相当糟糕。当然,您可以使用嵌套元组和类型同义词来解释它们的含义,但所有这些都是脆弱的,并且使用适当的记录类型会更好、更安全。正如您所确定的,向量的 x 和 y 分量通常会聚在一起。不仅如此,在许多意义上,让 x 和 y 分量对任何有趣的代码完全隐藏是一个好主意。这正是Vector类型应该完成的。(可能应该调用Vector3替代。)该vector函数的唯一目的应该是从组件中组装其中之一。好吧,如果这是它唯一要做的事情,那么这三个组件是唯一的争论,并且没有必要将它们进一步组合在一起......这基本上只是将一个手提箱放入另一个运输箱中。最好立即将正确的容器用作单个包装器。

vector3 :: Float -> Float -> Float -> Vector3
Run Code Online (Sandbox Code Playgroud)

常用函数签名中元组的一个例子是

randomR :: (Random a, RandomGen g) => (a,a) -> g -> (a,g)
Run Code Online (Sandbox Code Playgroud)

为什么这是一个坏主意?好吧,您使用元组来表示一个区间……但在结果中也表示完全不同的东西,即获得的随机值与更新后的生成器的分组。做到这一点的正确方法是拥有一个正确表达它的类型

data Interval a = Interval {lowerBound, upperBound :: a}
randomR :: (Random a, RandomGen g) => Interval a -> g -> (a,g)
Run Code Online (Sandbox Code Playgroud)

...或者更好的是,将关注点分开,即手动状态线程应该隐藏在合适的 monad 中——例如RVar. 此时范围限制成为唯一的参数,因此您不再需要将它们组合在一起!

uniform :: Distribution Uniform a => a -> a -> RVar a
Run Code Online (Sandbox Code Playgroud)

这并不意味着您根本不应该使用元组。对于结果值,柯里化机制并不容易,因此如果您有一个返回两个结果的函数,但对于这两个值一起表示的内容没有任何有意义的解释,那么,返回一个元组。

此外,如果您将完全抽象的类型组合在一起,则不可能对它们组合在一起的含义进行解释。这就是为什么zip :: [a] -> [b] -> [(a,b)]给出元组列表的原因。


可以也有元组多结果的函数。为此,您需要使用 continuation-passing 样式,例如splitAt :: Int -> [a] -> ([a],[a])become splitAt' :: Int -> [a] -> ([a] -> [a] -> r) -> r