我用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实现,它们会以某种方式回避这个问题。但是使用它们会破坏我学习的目的。
答案 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
和普通矩阵乘法实现标量乘法,因为你不知道要创建什么大小的矩阵 - 它将取决于它的上下文。
但是,如果您在类型系统中跟踪矩阵大小,则可以使用此方法。然后编译器可以推断出扩展标量的大小,并且它还可以在编译时检测矩阵运算的维度不匹配。