更好的组合破坏性运营商的界面

时间:2011-05-19 13:33:20

标签: haskell

我在我的一个旧项目中得到以下代码:

-- |ImageOperation is a name for unary operators that mutate images inplace.
newtype ImageOperation c d = ImgOp (Image c d-> IO ())

-- |Compose two image operations
(#>) :: ImageOperation c d-> ImageOperation c d -> ImageOperation c d
(#>) (ImgOp a) (ImgOp b) = ImgOp (\img -> (a img >> b img))

-- |An unit operation for compose 
nonOp = ImgOp (\i -> return ())

-- |Apply image operation to a Copy of an image
img <# op = unsafeOperate op img

-- | Apply the operation on a clone of an image
operate (ImgOp op) img = withClone img $ \clone -> 
                                           op clone >> return clone

unsafeOperate op img = unsafePerformIO $ operate op img

它的主要目的是允许组合运行到位的opencv运算符并接受相同格式和维度的图像。这是一个重要的优化,因为例如,如果没有它绘制100行将分配1mb图像数百次。当前界面运行良好,但我觉得可能有一些标准方法来做这样的事情。所以,

  1. 我是否在其他地方出现了标准名称?
  2. 我可以做得更好吗?
  3. 这种方法是否可以以不允许对可变图像的特定状态进行不安全引用的方式对二元运算符进行推广?
  4. 修改 二进制操作的一个示例是“拍摄图像,制作模糊的副本并从原始图像中减去”。返回结果'。仅在IO monad中具有最小副本的有效版本将类似于:

    poorMansHighPass img = do
        x <- clone img
        gaussian (5,5) x
        subtract x img
        return x
    

    虽然我可以创建这样的运算符,但我更喜欢的东西更多是原始运算符的组合而不是丑陋的不安全的io代码。

2 个答案:

答案 0 :(得分:5)

好吧,我至少可以指出你目前正在使用的一些模式。

所以我们有一个表示对某些可变数据的引用的类型,以及一个表示对它的不透明操作的类型。我们还有一个null op和一个组合函数,它提供了一个明显的Monoid实例:

instance Monoid (ImageOperation c d) where
    mempty = nonOp
    mappend = (#>)

这至少可以使用一个标准名称。

此外,上述Monoid实际上是其他两种众所周知类型属性的直接结果:

  • Applicative的{​​{1}}和/或Monad实例描述了通过将所有这些函数应用于单个参数来组合函数,就像组合函数中的图像一样。基本上是(->) a monad的轻量级内联版本。

  • Reader的{​​{1}}个实例,或者更确切地说是它所暗示的幺半群结构。通过将Monad的类型参数固定为IO,monad法则简化为一个简单的monoid,其中IO为单位,()为monoid操作。

为了重建你的组合,给定两个函数(展开return () s)并想象(>>)的隐含monoid是一个实际的实例,我们可以写:

ImageOperation

同样值得注意的是,读取器monad和monad允许可变状态之类的组合实质上描述了“具有可变引用的周围环境”,也称为可变全局变量,除了“global”在这里意味着“在单个计算中”合并monad“。我实际上是使用IO ()nonOp = pure mempty x #> y = mappend <$> x <*> y 明确构建了这样一个monad。

处理组合操作。要实际运行一个操作,你需要一个ReaderT并且我正在收集你想要只对克隆进行操作,其创建效率很低。幸运的是,考虑到STM的上述构造非常一般,在实际运行它之前,你真的没有什么东西可以塞进Image。生成克隆可能是一个Monoid操作,这是我假设在ImageOperation中进行的操作 - 可能没有其他任何方法可以做到这一点。

除此之外,如果你对构建整个事物的其他方法感兴趣,一个明显的变体就是将IO换成代表构造一个过程的东西,并将运算符合并以转换图像使用operate之类的东西制作。不过,我不知道这是否会让你获得任何东西。

事实上,我倾向于怀疑还有其他方法可以做到这一点。你正在编写一个FFI绑定到一个高度命令性的库,你只能做很多事情来伪装它。

但是,我不确定为什么你有Image的不安全版本。这有什么实际意义?

我也不确定你想要概括为什么类型的二元运算符 - 除了你在这里之外,你在operate上除了你之外别无他法。你的意思是推广operate来处理一个以上对图像的可变引用吗?或者涉及对图像进行操作的东西,这些图像返回的不仅仅是ImageOperation


编辑:好的,让我们来看看如何分解ImageOperation。希望我正确地阅读它在这里做的事情:

首先,IO ()是独立的,可以作为自己的操作进行考虑:poorMansHighPass

接下来,gaussian也可以被计算出来,并通过其他gauss' = ImgOp . gaussian subtract进行参数化。

这两个是函数的核心,它们可以通常的方式组合:Image。恢复原始函数所需的最后一件事是,subtr' = ImgOp . flip subtract的{​​{1}}参数必须是相同的图像,其克隆通过{{1}传递给内部函数}。

首先,我们将显式展开poorMansHP' img = gauss' (5, 5) #> subtr' img并在重新实现中使用它:

img

在此处替换poorMansHP'代替operate

ImageOperation

Desugar poorMansHighPass img = let (ImgOp op) = gauss' (5, 5) #> subtr' img in do x <- clone img op x return x 区块:

withClone

...显然包含clone的重新实现,因此请更换并简化:

poorMansHighPass img = 
    let (ImgOp op) = gauss' (5, 5) #> subtr' img
    in withClone img $ \x -> do op x
                                return x

更有趣的是以修改参数而不是克隆的方式实现do,这将允许将其打包为poorMansHighPass img = let (ImgOp op) = gauss' (5, 5) #> subtr' img in withClone img $ \x -> op x >> return x 本身。可能这是它应该做的,我误读了你的代码?

无论如何,重构的基本结构是相同的,但是你需要一个不同的组合运算符 - 而不是依次将两个运算符应用于同一个输入,它需要在内部创建一个输入的克隆在重新组合结果之前。我粗略地了解了哪种结构可以使这项工作顺利进行,如果您愿意,我可以详细说明,但我必须稍微努力以确保其正常运行。

答案 1 :(得分:2)

Here是我使用OpenCV代码的方法。我从来没有看到它超出了我正在进行的几个项目的需要,我认为这种方法应该伴随性能回归测试。