标量乘法与Haskell中的新Matrix类型

时间:2014-01-25 20:16:31

标签: haskell matrix typeclass

我用C型和Lisp型语言编程了几十年,而Haskell用了几年。现在为了进一步理解类型和Haskell的其他漂亮的高级功能,例如并行性,我一直在尝试创建一个带有矩阵语义的新数据类型,现在可以在库的Array类型之上进行背负。

我正在使用最新的Haskell Platform 2013.2.0.0和附带的ghc。

newtype Matrix a = Matrix (Array (Int,Int) a)

(我知道数据可以起作用而不是newtype,但在这种情况下,你会得到相同的语义,而使用newtype会有更好的性能)。

创建单位矩阵的一些简单函数,例如,工作得很好:

unityMatrix:: (Num a) => Int -> Matrix a
unityMatrix d = Matrix $ let b = ((0,0),(d-1,d-1)) in array b
    [((i,j),if i==j then 1 else 0)|(i,j)<-range b]

创建基本的show函数也是如此:

instance Show a => Show (Matrix a) where
    show (Matrix x) =
        let
            b = bounds x
            rb = ((fst.fst) b, (fst.snd) b)
            cb = ((snd.fst) b, (snd.snd) b)
        in
            intercalate "\n" [unwords ([show (x!(r,c))|c<-range cb])|r<-range rb]

然后,为了使常规算术运算符起作用,我添加了这个:

instance Num a => Num (Matrix a) where
    fromInteger x =
        let
            b = ((0,0),(0,0))
        in Matrix $ array b
            [((i,j),if i == j then (fromInteger x) else 0)|(i,j)<-range b]

    (Matrix x) + (Matrix y) =
        let
            b = bounds x
        in
            if b /= bounds y then error "Unmatched matrix addition" else Matrix $ array b
                [(ij,x!ij + y!ij)|ij<-range b]

    signum (Matrix x) =
        let
            b = bounds x
        in Matrix $ array b
            [(ij,signum (x!ij))|ij<-range b]

    abs (Matrix x) =
        let
            b = bounds x
        in Matrix $ array b
            [(ij,abs (x!ij))|ij<-range b]

    (Matrix x) - (Matrix y) =
        let
            b = bounds x
        in
            if b /= bounds y then error "Unmatched matrix subtraction" else Matrix $ array b
                [(ij,x!ij - y!ij)|ij<-range b]

    (Matrix x) * (Matrix y) =
        let
            b = (((fst.fst.bounds) x, (fst.snd.bounds) x),((snd.fst.bounds) y, (snd.snd.bounds) y))
            kb = ((snd.fst.bounds) x, (snd.snd.bounds) x)
        in
            if kb /= ((fst.fst.bounds) y, (fst.snd.bounds) y) then error "Unmatched matrix multiplication" else  Matrix $ array b
                [((i,j),sum [(x!(i,k)) * (y!(k,j))|k<-range kb])|(i,j)<-range b]

(如果这里的缩进被搞砸了,我很抱歉 - 实际代码正确缩进并编译。)

到目前为止,非常好,虽然有必要定义一个在矩阵语义中没有任何意义的fromInteger函数,但创建一个带有该值的1x1矩阵与其他任何东西一样合理有点烦人。

我的问题是尝试使用矩阵来获得标量(即Num类型类型)的乘法的正确语义。通过数学约定,这意味着只用元素方式乘以标量。

然而,无论我尝试什么语法,我都没有得到正确的结果。默认情况下,此实现仅使用fromInteger将Int(例如,Int)提升为矩阵(除非Matrix已经是1x1),这会导致“不匹配的矩阵乘法”错误。我已经尝试了所有替代语法,我可以想到为这种类型的乘法定义替代代码但没有成功,例如:

(Matrix x) * (y::Int) =

(Matrix x) * (Num y) =

(*):: (Num a) => Matrix -> a -> Matrix

但所有这些都给了我各种语法错误。

我应该如何通过矩阵乘法来定义标量,以便它能够按照您的预期进行操作?我觉得启用非标准模式防护功能可能有所帮助,但我不太确定如何在这种情况下正确使用它。

我意识到我可以在特殊情况下使用矩阵乘法来允许任何矩阵与1x1矩阵相乘,我想这将起作用。但那将是(a)不优雅,(b)un-Haskell-y,(c)可能是低效的,因为它需要在乘法之前将每个标量包装到矩阵中,并且(d)允许某些代码(例如任意矩阵与任何1x1矩阵的乘法运算,以便在它应该导致错误时运行。

我也明白,那里可能有很好的Matrix实现,它们会以某种方式回避这个问题。但是使用它们会破坏我学习的目的。

4 个答案:

答案 0 :(得分:4)

这是标准(*)运算符的类型:

(*) :: Num a => a -> a -> a

换句话说,此运算符的两个参数具有相同的类型,因此您所要求的是不可能的。

我可以看到几个选项:

  • 正如评论中所建议的那样,定义您自己的(*),也许还有一个类型类,以便像Integer这样的标准类型也可以成为其中的一员。 This answer可能会提供一些灵感。

  • 将标量添加到矩阵类型:

    data Matrix a = Scalar a | Matrix (Array (Int,Int) a)

    (也许这个名字现在可以改进了 - 同时请注意,现在它必须是data而不是newtype。我怀疑性能差异在实践中是否重要。)

答案 1 :(得分:3)

只是尝试延长Ganesh's answer。 我们可以将标量矩阵重新定义为未确定大小的Unity矩阵。

import Data.List (transpose)

data Matrix a = Matrix [[a]] | UnityMatrix a deriving (Show)

instance Functor Matrix where
    fmap f (Matrix x) = Matrix $ fmap (fmap f) x
    fmap f (UnityMatrix x) = UnityMatrix $ f x 

fmap2::(Num a) => (a->a->b)->Matrix a->Matrix a->Matrix b    
fmap2 f (Matrix x) (Matrix y) = Matrix $ zipWith ( zipWith f ) x y
fmap2 f m@(Matrix x) u@(UnityMatrix  y) = fmap2 f m $ expandUnity u
fmap2 f u@(UnityMatrix  y) m@(Matrix x) = fmap2 f (expandUnity u) m
fmap2 f (UnityMatrix x) (UnityMatrix y) = UnityMatrix $ f x y

expandUnity (UnityMatrix a) = Matrix [replicate i 0 ++ a : repeat 0| i <-[0..]]

instance Num a => Num (Matrix a) where
    fromInteger = UnityMatrix . fromInteger

    signum = fmap signum
    abs = fmap abs

    (+) = fmap2 (+)
    (-) = fmap2 (-)    

    (Matrix x) * (Matrix y) = Matrix [[sum $ zipWith (*) a b | b <- transpose y ]| a <- x ]
    m@(Matrix x) * (UnityMatrix y) = fmap (*y) m
    (UnityMatrix y) * m@(Matrix x) = fmap (y*) m
    (UnityMatrix x) * (UnityMatrix y) = UnityMatrix $ x * y

main = print $  3 * Matrix [[1,2,3],[4,5,6]]  + 2

此代码包含大量重复 - 但如果基类型中的任何(+),( - )或(*)运算符不可交换,则会出现这种情况。 底层类型也改为列表而不是矩阵。但这只是为了简化想法演示。

答案 2 :(得分:2)

“您正在寻找的语义”与Haskell Num类的定义方式不一致。在Haskell中没有数据推广这样的东西(非常好的理由很难解释,但很容易找到你用Haskell获得的更多经验)。

我不建议定义新的*,也不建议您自己的运营商名称具有签名a -> Matrix a -> Matrix a,这会让人感到困惑。相反,您应该查看已定义此操作的位置:正如user5402所拥有的那样,这是标量乘法。这显然不仅对矩阵/线性算子有意义,而且已经对向量有意义。不料,here is the class for vector spaces

但是就像Ganesh和Odomontois对Num的建议一样,您还需要扩展数据类型以正确使用它。基本问题是矩阵乘法实际上只是为匹配协变 - 逆变维度而定义,但是你的方法无法确保这一点。理想情况下,类型检查器应该推断出正确的维度,但除此之外,您可以有一个特殊情况,不仅仅是身份而是一般的对角矩阵。

data LinOp a = Diagonal a
             | GMatrix (Matrix a)

instance Functor LinOp where
  fmap f (Diagonal a) = Diagonal (f a)
  fmap f (GMatrix m) = GMatrix $ fmap f m

instance (Num a) => AdditiveGroup (Matrix a) where
  zeroV = Diagonal 0
  negateV = fmap negate
  (Diagonal x) ^+^ (Diagonal y) = Diagonal $ x+y
  ...

instance (Num a) => VectorSpace (Matrix a) where
  type Scalar (Matrix a) = a
  μ *^ m = fmap (μ*) m

instance (Num a) => Num (Matrix a) where
  fromInteger = Diagonal . fromInteger
  (+) = (^+^)
  ...

答案 3 :(得分:1)

这有用吗?

scalarMult :: Num a => a -> Matrix a -> Matrix a
scalarMult c (Matrix x) = Matrix $ array (range x) [(ij,(x!ij) * c) | ij <- range x]

一般来说,你不能用fromInteger和普通矩阵乘法实现标量乘法,因为你不知道要创建什么大小的矩阵 - 它将取决于它的上下文。

但是,如果您在类型系统中跟踪矩阵大小,则可以使用此方法。然后编译器可以推断出扩展标量的大小,并且它还可以在编译时检测矩阵运算的维度不匹配。