为什么不是 sum == foldl1 (+)?

Why isn't sum == foldl1 (+)?

我用 foldl1 (+) 对矩阵列表求和,因为我注意到 sum 实际上 returns 将左上角元素的总和作为 1x1 矩阵。

$ stack exec --resolver lts-12.5 --package matrix -- ghci
GHCi, version 8.4.3: http://www.haskell.org/ghc/  :? for help
Prelude> import Data.Matrix
Prelude Data.Matrix> t = [identity 2, identity 2]  -- two 2x2 identity matrices
Prelude Data.Matrix> foldl1 (+) t
┌     ┐
│ 2 0 │
│ 0 2 │
└     ┘
Prelude Data.Matrix> sum t
┌   ┐
│ 2 │
└   ┘

这对我来说是出乎意料的,LYAH 建议 sum = foldl1 (+)¹,甚至 hlint 建议我使用 sum 而不是 foldl1 (+),因为 'removes error on []'(附带问题:如何我优雅地 []- 安全地求和矩阵?)

为什么 sum /= foldl1 (+) 用于矩阵,为什么通常不总是 sum == foldl1 (+)

或者,排除空列表的情况:

为什么不是 sum == foldl neutralElement (+)?或具体 sum == foldl (+) (zero 2 2)[identity 2, identity 2]?

自己的作品:

Prelude Data.Matrix> :t sum
sum :: (Foldable t, Num a) => t a -> a
Prelude Data.Matrix> :info sum
class Foldable (t :: * -> *) where
  ...
  sum :: Num a => t a -> a
  ...
        -- Defined in ‘Data.Foldable’

sum 的来源是:

sum :: Num a => t a -> a
sum = getSum #. foldMap Sum

Matrix 的实例是:

instance Foldable Matrix where
foldMap f = foldMap f . mvect

instance Num a => Num (Matrix a) where
 fromInteger = M 1 1 . V.singleton . fromInteger
 negate = fmap negate
 abs = fmap abs
 signum = fmap signum

 -- Addition of matrices.
 {-# SPECIALIZE (+) :: Matrix Double -> Matrix Double -> Matrix Double #-}
 {-# SPECIALIZE (+) :: Matrix Int -> Matrix Int -> Matrix Int #-}
 (M n m v) + (M n' m' v')
   -- Checking that sizes match...
   | n /= n' || m /= m' = error $ "Addition of " ++ sizeStr n m ++ " and "
                               ++ sizeStr n' m' ++ " matrices."
   -- Otherwise, trivial zip.
   | otherwise = M n m $ V.zipWith (+) v v'

 -- Substraction of matrices.
 ...
 -- Multiplication of matrices.
 ...

¹ 在整数相加的上下文中

开始吧:

why isn't generally always sum == foldl1 (+)?

foldl1 (+) []
*** Exception: Prelude.foldl1: empty list

sum []
0

这是主要区别,sum 允许与 [] 进行模式匹配,而 foldl1 (+) 则不允许。

foldl1 用于非空列表,因为有些函数对空列表没有意义,例如:

foldl1 max

[]max 是什么?没有最大值,因为没有元素。

foldl1 max [5,4,2,3]
5

因为 NumData.Matrix 实例通过返回 1x1 矩阵实现 fromInteger,而 + 通过添加 elementwise 来实现 elementwise左边的大小(如果左边大于右边则出错)。

sumfoldl (+) 0,它以 0 中的 1x1 矩阵开始,并将列表中的所有矩阵截断为该大小。 foldl1 (+) 不必从整数生成矩阵,因此结果是列表中最小矩阵的大小。

此外,sum = getSum #. foldMap Sum 是默认的 Foldable 实现,但它被列表实例覆盖为 sum = foldl (+) 0

如果操作数的形状不同,+(如上所示)的 Data.Matrix 实现(如上所示)到版本 0.3.1.1 会产生错误,但在版本 0.3.2.0 中它假设没有检查它们是相同的形状,并且在版本 0.3.3.0 中它再次更改(根据评论):

-- | Perform an operation element-wise.
--   The second matrix must have at least as many rows
--   and columns as the first matrix. If it's bigger,
--   the leftover items will be ignored.
--   If it's smaller, it will cause a run-time error.
--   You may want to use 'elementwiseUnsafe' if you
--   are definitely sure that a run-time error won't
--   arise.

直到当前版本 0.3.6.1

,实现似乎都是一样的

我认为@pat 已经很好地回答了这个问题,但我会尝试回答附带的问题:

How do I elegantly and []-safely sum up matrices?

您不能安全地汇总矩阵列表,因为您不确定列表中每个矩阵的大小是多少,除非您在类型级别具有该大小信息。想象一个类型 data Matrix (m :: Nat) (n :: Nat) e = ...,那么可以保证列表中的每个矩阵都具有完全相同的维度。我不确定通用数组库在该领域是否有任何工作,但我在 OpenCV bindings and in mapalgebra and briefly considered using it in massiv 中看到过这种方法。这种方法有一些缺点,但这对这个问题并不重要。

另一种方法是不使用矩阵列表,而是使用 3D 数组,它本质上是一系列大小相同的 2D 矩阵页面。显然,matrix 包不支持更高维度,因此您无法使用该库实现它。不过,我将展示如何使用 massiv 来完成。考虑我们有这个 3D 数组 a:

λ> a = makeArrayR D Par (Sz3 3 4 5) $ \(i :> j :. k) -> i + j * k
λ> a
Array D Par (Sz (3 :> 4 :. 5))
  [ [ [ 0, 0, 0, 0, 0 ]
    , [ 0, 1, 2, 3, 4 ]
    , [ 0, 2, 4, 6, 8 ]
    , [ 0, 3, 6, 9, 12 ]
    ]
  , [ [ 1, 1, 1, 1, 1 ]
    , [ 1, 2, 3, 4, 5 ]
    , [ 1, 3, 5, 7, 9 ]
    , [ 1, 4, 7, 10, 13 ]
    ]
  , [ [ 2, 2, 2, 2, 2 ]
    , [ 2, 3, 4, 5, 6 ]
    , [ 2, 4, 6, 8, 10 ]
    , [ 2, 5, 8, 11, 14 ]
    ]
  ]

然后我们可以使用 foldlWithin 折叠数组的任何维度,同时减少它的维数:

λ> foldlWithin Dim3 (+) 0 a
Array D Par (Sz (4 :. 5))
  [ [ 3, 3, 3, 3, 3 ]
  , [ 3, 6, 9, 12, 15 ]
  , [ 3, 9, 15, 21, 27 ]
  , [ 3, 12, 21, 30, 39 ]
  ]
λ> foldlWithin Dim1 (+) 0 a
Array D Par (Sz (3 :. 4))
  [ [ 0, 10, 20, 30 ]
  , [ 5, 15, 25, 35 ]
  , [ 10, 20, 30, 40 ]
  ]

请注意,不会出现异常或其他一些不可预知的行为,就像您在此处任何时候遇到的那样(当然,将异步异常放在一边)。或者,可以获取数组的切片,然后单独添加这些切片,但是如果我们想避免异常,这种方法会涉及更多:

λ> a !?> 0
Array D Par (Sz (4 :. 5))
  [ [ 0, 0, 0, 0, 0 ]
  , [ 0, 1, 2, 3, 4 ]
  , [ 0, 2, 4, 6, 8 ]
  , [ 0, 3, 6, 9, 12 ]
  ]
λ> import Data.Maybe
λ> fromMaybe empty $ a !?> 0 >>= \acc0 -> foldlM (\acc pageIx -> (acc +) <$> (a !?> pageIx)) acc0 (1 ... 2)
Array D Par (Sz (4 :. 5))
  [ [ 3, 3, 3, 3, 3 ]
  , [ 3, 6, 9, 12, 15 ]
  , [ 3, 9, 15, 21, 27 ]
  , [ 3, 12, 21, 30, 39 ]
  ]

如果 a 没有索引为 02 的页面,上面将生成 Nothing。另一方面,如果您对运行时异常没问题,那么这会更清楚一些,但不那么安全:

λ> foldlS (\acc pageIx -> acc + (a !> pageIx)) (a !> 0) (1 ... 2)
Array D Par (Sz (4 :. 5))
  [ [ 3, 3, 3, 3, 3 ]
  , [ 3, 6, 9, 12, 15 ]
  , [ 3, 9, 15, 21, 27 ]
  , [ 3, 12, 21, 30, 39 ]
  ]

或使用列表,如实际问题中所述:

λ> Prelude.foldl1 (+) $ Prelude.fmap (a !>) [0 .. 2]
Array D Par (Sz (4 :. 5))
  [ [ 3, 3, 3, 3, 3 ]
  , [ 3, 6, 9, 12, 15 ]
  , [ 3, 9, 15, 21, 27 ]
  , [ 3, 12, 21, 30, 39 ]
  ]

旁注。我注意到 SECIALIZE pragma 的用法,这使我相信您实际上关心性能。一个小而重要的事实:matrix 包在 Matrix 数据类型下面使用盒装向量,这总是会给你带来糟糕的性能,没有 pragma 可以帮助你。