了解可变向量以及如何在 Haskell 中修改它们

Understanding mutable vectors and how to modify them in Haskell

我正在查看 JuicyPixels 中的一些源代码,可变向量让我感到困惑。在第 808 行的函数 generateMutableImage 中是

generateMutableImage :: forall m px. (Pixel px, PrimMonad m)
                     => (Int -> Int -> px)  -- ^ Generating function, with `x` and `y` params.
                     -> Int        -- ^ Width in pixels
                     -> Int        -- ^ Height in pixels
                     -> m (MutableImage (PrimState m) px)
{-# INLINE generateMutableImage #-}
generateMutableImage f w h = MutableImage w h `liftM` generated where
  compCount = componentCount (undefined :: px)

  generated = do
    arr <- M.new (w * h * compCount)
    let lineGenerator _ !y | y >= h = return ()
        lineGenerator !lineIdx y = column lineIdx 0
          where column !idx !x | x >= w = lineGenerator idx $ y + 1
                column idx x = do
                    unsafeWritePixel arr idx $ f x y
                    column (idx + compCount) $ x + 1
    lineGenerator 0 0
    return arr

我以为generatedm (Data.Vector.Storable.Mutable.STVector)类型的,也知道是怎么声明的,但不知道怎么修改的。

我知道 unsafeWritePixel 负责这个,但它与 (PrimMonad m) => m monad 有什么联系?

另外,!y!是什么意思?这些是一些神奇的运算符或符号吗?

片段

let lineGenerator _ !y | y >= h = return ()
    lineGenerator !lineIdx y = column lineIdx 0
      where column !idx !x | x >= w = lineGenerator idx $ y + 1
            column idx x = do
                unsafeWritePixel arr idx $ f x y
                column (idx + compCount) $ x + 1
lineGenerator 0 0

是一种循环,在向量中写入图像数据。

这是通过定义一个递归函数 lineGenerator 然后用 lineGenerator 0 0 中的起始值调用它来实现的。第一个参数是索引,它指的是我们正在写的向量的位置,并且在递归过程中经常递增。第二个参数表示图像的 y 坐标。两者都从 0.

开始

让我们逐行详细评论:

let lineGenerator _ !y | y >= h = return ()

如果当前y比高度>=,我们停止循环。

    lineGenerator !lineIdx y = column lineIdx 0

否则,填一行。让x0.

开始
      where column !idx !x | x >= w = lineGenerator idx $ y + 1

如果 x 已达到宽度 w,我们填充了该行,因此通过递增 y 移动到下一个。

            column idx x = do

否则,我们需要在当前行写入一个像素。

                unsafeWritePixel arr idx $ f x y

写在当前索引处,根据xy的当前值使用f x y计算图像数据。

                column (idx + compCount) $ x + 1

然后,移动到下一个像素,同时推进 x 坐标和矢量索引(这样我们就不会覆盖刚刚写入的像素)。

一切似乎都在 m monad 中执行,这是必需的,因为我们需要改变向量。

您可以安全地忽略 !y 和其他变量中的 !。这些被称为“bang patterns”,在这里用作优化,以确保像 x+1 这样的数值被立即计算出来,而不是保持未评估状态。 Haskell 默认情况下是懒惰的,除非现在需要,否则不会计算任何东西,但这可能会影响性能,因此有时程序员会使用 bang 模式手动禁用惰性。同样,在阅读代码时,您可以假装这些 ! 刘海不存在。