Haskell:在列表和元组之间

mk1*_*k12 15 math haskell functional-programming tuples list

我想要一个+++添加两个数学向量的函数.

我可以实现向量[x, y, z]并使用:

(+++) :: (Num a) => [a] -> [a] -> [a]
(+++) = zipWith (+)
Run Code Online (Sandbox Code Playgroud)

因此适应任何n维向量(所以这也适用[x, y]).

或者我可以实现向量(x, y, z)并使用:

type Triple a = (a, a, a)

merge :: (a -> b -> c) -> Triple a -> Triple b -> Triple c
merge f (a, b, c) (x, y, z) = (f a x, f b y, f c z)

(+++) :: (Num a) => Triple a -> Triple a -> Triple a
(+++) = merge (+)
Run Code Online (Sandbox Code Playgroud)

当然这稍微复杂一点,但是当我实现所有其他向量函数时,这是无关紧要的(50行而不是40行).

列表方法的问题是我可以添加带有3D矢量的2D矢量.在这种情况下,zipWith只需切断3D矢量的z组件.虽然这可能有意义(更可能是它应该将2D矢量扩展到[x, y, 0]),但对于其他函数,我认为要么无声地发生它可能会有问题.元组方法的问题是它将向量限制为3个组件.

直觉上,我认为将向量表示为更有意义(x, y, z),因为数学向量具有固定数量的组件,并且将组件合并(前置)到向量上并不真正有意义.

另一方面,尽管除了3D矢量之外我不太可能需要任何其他东西,但将它限制在此范围内似乎并不正确.

我想我想要的是带有两个相同长度或更好的函数的函数,这些函数对任意大小的元组进行操作.

任何建议,在实用性,可扩展性,优雅等方面?

Lan*_*dei 21

您可以使用类型级编程.首先,我们需要将每个自然数作为一个单独的类型.继自然数的皮亚诺的定义,Z0S xx + 1

data Z = Z
data S a = S a

class Nat a
instance Nat Z
instance (Nat a) => Nat (S a)
Run Code Online (Sandbox Code Playgroud)

现在我们可以使用一个类型Vec来简单地包装一个列表,但是通过使用来跟踪它的大小Nat.为此,我们使用智能构造 nil<:>(所以你不应该导出数据构造Vec从你的模块)

data Vec a = Vec a [Int]

nil = Vec Z []

infixr 5 <:>
x <:> (Vec n xs) = Vec (S n) (x:xs)
Run Code Online (Sandbox Code Playgroud)

现在我们可以定义一个add函数,它要求两个向量具有相同的Nat:

add :: Nat a => Vec a -> Vec a -> Vec a
add (Vec n xs) (Vec _ ys) = Vec n (zipWith (+) xs ys) 
Run Code Online (Sandbox Code Playgroud)

现在你有一个带有长度信息的矢量类型:

toList (Vec _ xs) = xs
main = print $ toList $ add (3 <:> 4 <:> 2 <:> nil) (10 <:> 12 <:> 0 <:> nil) 
Run Code Online (Sandbox Code Playgroud)

当然,这里具有不同长度的向量将导致编译错误.

这是易于理解的版本,有更短,更高效和/或更方便的解决方案.


lef*_*out 14

最简单的方法是将+++运算符放在类型类中,并创建各种元组大小实例:

{-# LANGUAGE FlexibleInstances #-}   -- needed to make tuples type class instances

class Additive v where
  (+++) :: v -> v -> v

instance (Num a) => Additive (a,a) where
  (x,y) +++ (?,?)  =  (x+?, y+?)
instance (Num a) => Additive (a,a,a) where
  (x,y,z) +++ (?,?,?)  =  (x+?, y+?, z+?)
...
Run Code Online (Sandbox Code Playgroud)

这样,可以添加可变长度的元组,但是在编译时将确保双方总是具有相同的长度.


通用它以使用类似于merge实际类型类的函数也是可能的:在这种情况下,您需要将类实例指定为类型构造函数(如列表monad).

class Mergable q where
  merge :: (a->b->c) -> q a -> q b -> q c

instance Mergable Triple where
  merge f (x,y,z) (?,?,?) = (f x ?, f y ?, f z ?)
Run Code Online (Sandbox Code Playgroud)

然后简单地说

(+++) :: (Mergable q, Num a) => q a -> q b -> q c
+++ = merge (+)
Run Code Online (Sandbox Code Playgroud)

不幸的是,这不太有用,因为类型同义词可能不会被部分评估.你需要制作Triple一个新类型,比如

newtype Triple a = Triple(a,a,a)
Run Code Online (Sandbox Code Playgroud)

然后

instance Mergable Triple where
  merge f (Triple(x,y,z)) (Triple((?,?,?)) = Triple(f x ?, f y ?, f z ?)
Run Code Online (Sandbox Code Playgroud)

这当然不是很好看.

  • @VladtheImpala:也许你更喜欢[日语](http://codegolf.stackexchange.com/a/4824/2183)? - 说真的,调用局部变量希腊名字有什么问题?它迫使没有人在他们自己的代码中键入它们,如果你知道希腊字母表将z与zeta联系起来是有意义的,如果你不这样做,那么与任意拉丁字母相比,它几乎没有什么区别. (3认同)

mk1*_*k12 -1

Landei 和 leftaroundabout 的答案很好(感谢你们俩),我想我应该意识到这不会像我希望的那么简单。尝试执行我建议的任何一个选项都会产生复杂的代码,这本身不会成为问题,只是用户代码看起来也不是很漂亮。

\n\n

我想我已经决定使用元组并坚持仅使用 3 维向量,仅仅是因为它在语义上看起来比使用列表更正确。不过,我最终会重新实现mapzipWithsum其他三元组。我想坚持简单性\xe2\x80\x94我觉得好像我有一个令人信服的论点将向量视为列表,那么该解决方案会更好(前提是我确保不混合维度)\xe2\x80 \xa6 但是,当我实际使用向量时,函数将采用 3d 向量作为参数,而不是可变维度之一,并且Num a => [a]无法强制执行这一点。

\n

  • 库的代码可能很复杂,但是您可以使用“类型”定义和便捷方法之类的东西很好地向用户隐藏它。 (3认同)