我想要一个函数+++
,它会添加两个数学向量。
我可以将矢量实现为[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行)。
列表方法的问题是我可以添加带有3D矢量的2D矢量。在这种情况下,zipWith
只会切断3D矢量的z
组件。虽然这可能有意义(更有可能它应该将2D向量扩展为[x, y, 0]
),但对于其他函数,我认为要么无声地发生这些问题可能会有问题。元组方法的问题是它将向量限制为3个组件。
直观地说,我认为将向量表示为(x, y, z)
会更有意义,因为数学向量具有固定数量的组件,并且将组件的缺点(前置)实际上没有意义矢量。
另一方面,尽管除了3D矢量之外我不太可能需要任何其他东西,但将它限制在那种情况似乎并不正确。
我想我想要的是带有两个相等长度或更好的函数的函数,这些函数对任意大小的元组进行操作。
任何建议,在实用性,可扩展性,优雅等方面?
答案 0 :(得分:21)
您可以使用类型级编程。首先,我们需要将每个自然数作为一个单独的类型。根据Peano对自然数的定义,Z
为0
,S x
为x + 1
data Z = Z
data S a = S a
class Nat a
instance Nat Z
instance (Nat a) => Nat (S a)
现在我们可以使用类型Vec
来简单地包装列表,但是使用Nat
来跟踪其大小。为此,我们使用smart constructors nil
和<:>
(因此您不应从模块中导出数据构造函数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)
当然,这里有不同长度的向量会导致编译错误。
这是易于理解的版本,有更短,更高效和/或更方便的解决方案。
答案 1 :(得分: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+ζ)
...
这样,可以添加可变长度的元组,但是在编译时将确保双方总是具有相同的长度。
<小时/> 推广它以在实际类型类中使用类似
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 ζ)
然后简单地
(+++) :: (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 ζ)
当然不太好看。
答案 2 :(得分:1)
由于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。
答案 3 :(得分:-1)
Landei和左撇子的答案都很好(感谢你们两个),我想我应该意识到这不会像我希望的那么简单。试图做我建议的任何一个选项,复杂的代码,这本身不是一个问题,除了看起来用户代码看起来不是很漂亮。
我认为我决定使用元组并坚持使用仅三维向量,因为它似乎比使用列表更具语义正确性。不过,我最终会重新实现map
,zipWith
,sum
和其他人的三重奏。我想坚持简单 - 我觉得如果我有一个令人信服的论据,将矢量视为列表,那么该解决方案将更好地工作(假设我确保我不混合维度)...当我实际使用向量时,但是,函数将3d矢量作为参数,而不是变量维度之一,Num a => [a]
不能强制执行。