【问题标题】:Haskell: Between a list and a tupleHaskell:在列表和元组之间
【发布时间】:2012-05-24 12:16:57
【问题描述】:

我想要一个函数+++,它将两个数学向量相加。

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

(+++) :: (Num a) => [a] -> [a] -> [a]
(+++) = zipWith (+)

因此可以容纳任何 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 (+)

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

列表方法的问题是我可以将 2D 向量与 3D 向量相加。在这种情况下,zipWith 将简单地切断 3D 矢量的z 组件。虽然这可能是有道理的(更有可能它应该将 2D 向量扩展为 [x, y, 0]),但对于其他功能,我认为静默发生可能会有问题。元组方法的问题在于它将向量限制为 3 个分量。

直观地说,我认为将向量表示为(x, y, z) 会更有意义,因为数学向量具有固定数量的分量,并且将分量添加到向量中并没有意义.

另一方面,虽然我不太可能需要除 3D 矢量以外的任何东西,但将其限制在此似乎不太正确。

我想我想要的是具有两个相等长度的列表的函数,或者更好的是,对任意大小的元组进行操作的函数。

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

【问题讨论】:

标签: list math haskell functional-programming tuples


【解决方案1】:

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

data Z = Z
data S a = S a

class Nat a
instance Nat Z
instance (Nat a) => Nat (S a)

现在我们可以使用Vec 类型来简单地包装一个列表,但可以使用Nat 来跟踪它的大小。为此,我们使用smart constructorsnil<:>(因此您不应从模块中导出数据构造函数Vec

data Vec a = Vec a [Int]

nil = Vec Z []

infixr 5 <:>
x <:> (Vec n xs) = Vec (S n) (x:xs)

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

add :: Nat a => Vec a -> Vec a -> Vec a
add (Vec n xs) (Vec _ ys) = Vec n (zipWith (+) xs ys) 

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

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

当然这里有不同长度的向量会导致编译错误。

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

【讨论】:

【解决方案2】:

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

{-# 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+ζ)
...

这样,可以添加可变长度的元组,但会在编译时确保双方始终具有相同的长度。


将其概括为在实际类型类中使用像 merge 这样的函数也是可能的:在这种情况下,您需要将类实例指定为类型构造函数(如 list 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 ζ)

然后简单

(+++) :: (Mergable q, Num a) => q a -> q b -> q c
+++ = merge (+)

不幸的是,这并不完全有效,因为类型同义词可能不会被部分评估。您需要将Triple 改为新类型,例如

newtype Triple a = Triple(a,a,a)

然后

instance Mergable Triple where
  merge f (Triple(x,y,z)) (Triple((ξ,υ,ζ)) = Triple(f x ξ, f y υ, f z ζ)

这当然不是很好看。

【讨论】:

  • @VladtheImpala:也许你更喜欢Japanese? — 说真的,将局部变量称为希腊名称有什么问题?它迫使没有人用他们自己的代码输入它们,如果你知道希腊字母,那么关联例如是有意义的。 z 与 zeta,如果你不这样做,它与任意拉丁字母相比几乎没有什么区别。
【解决方案3】:

由于 OP 想要一种更轻量级的方法,我会使用关联类型。

class VecMath a b where
    type Res a b :: *
    (+++) :: a -> b -> Res a b

instance Num a => VecMath (a,a,a) (a,a,a) where
    type Res (a,a,a) (a,a,a) = (a,a,a)
    (x1,y1,z1) +++ (x2,y2,z2) = (x1+x2, y1+y2, z1+z2)

instance Num a => VecMath (a,a) (a,a,a) where
    type Res (a,a) (a,a,a) = (a,a,a)
    (x1,y1) +++ (x2,y2,z) = (x1+x2, y1+y2, z)

instance Num a => VecMath (a,a,a) (a,a) where
    type Res (a,a) (a,a,a) = (a,a,a)
    -- (+++) analog
instance Num a => VecMath (a,a) (a,a) where
    type Res (a,a) (a,a) = (a,a)
    -- ...

Res 是一个类型函数,这里本质上导致它的参数的“更大”类型。优点是您仍然可以使用普通的旧元组,就好像 VecMath 不存在一样。如果您考虑将新类型添加到Res 的域中,那么黑暗的一面是您必须编写的实例呈指数级增长。 如需更多信息,请参阅this

【讨论】:

    【解决方案4】:

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

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

    【讨论】:

    • 使用 Data.Vector 来自 vector 包或具有 3D 矢量的 ACVector 包。这些库已经定义了辅助函数,可以节省您的时间和精力。
    • 库的代码可能很复杂,但您可以使用 type 定义和便捷方法等方法很好地向用户隐藏它。
    猜你喜欢
    • 1970-01-01
    • 2023-03-18
    • 1970-01-01
    • 1970-01-01
    • 2017-07-24
    • 2018-04-24
    • 2016-07-31
    • 2015-08-11
    • 1970-01-01
    相关资源
    最近更新 更多