问题
I've got the following code in an old project of mine:
-- |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
Its main purpose is to allow composition of opencv operators that run in place and accept an image of the same format and a dimension. It is an important optimization, since for example, without it drawing 100 lines would allocate the 1mb image hundred times. The current interface works nicely but I have a feeling that there might be a some standard approach to doing thing like this. So,
- Am I doing something that appears elsewhere with standardized name?
- Can I do this better?
- Can this approach be generalized for binary operators in a way that doesn't allow unsafe references to specific states of the mutable image?
Edit: An example of a binary operation is 'take an image, make a blurred copy and subtract from the original. Return the result'. The effective version with minimal copies in just the IO monad would be something like:
poorMansHighPass img = do
x <- clone img
gaussian (5,5) x
subtract x img
return x
Although I can make an operator like this, I would much prefer something that is more of a composition of primitive operators than ugly bit of unsafe io code.
回答1:
Well, I can at least point out what to call some of the patterns you're using currently.
So we have a type representing a reference to some mutable data, and a type representing opaque operations on it. We also have a null op and a composition function, which gives an obvious Monoid
instance:
instance Monoid (ImageOperation c d) where
mempty = nonOp
mappend = (#>)
So that's at least one standard name you could use.
Further, the above Monoid
is actually a straightforward result of the properties of two other well-known types:
The
Applicative
and/orMonad
instance for(->) a
describes combining functions by applying all of them to a single argument, as with the image in the composition function. Basically a lightweight, in-line version of theReader
monad.The
Monad
instance forIO
, or rather the monoidal structure it implies. By fixingIO
's type parameter to()
, the monad laws reduce to a simple monoid, withreturn ()
as unit and(>>)
as the monoid operation.
To reconstruct your combination, given two functions (unwrapped ImageOperation
s) and imagining that the implied monoid for IO ()
is an actual instance, we could write:
nonOp = pure mempty
x #> y = mappend <$> x <*> y
It's also worth noting that the combination of something like a reader monad and a monad allowing mutable state essentially describes "a surrounding environment with mutable references", a.k.a. mutable global variables, except that "global" here means "within a single computation of the combined monad". I've actually constructed such a monad explicitly, using ReaderT
and STM
.
That handles combining operations. To actually run an operation, you need an Image
and I'm gathering you want to only operate on clones, the creation of which is inefficient. Fortunately, considering how very general the above construction for the Monoid
is, there's really nothing you can't cram into an ImageOperation
before actually running it. Generating the clone is presumably an IO
operation and is what I assume is going on in operate
--there's probably not really any other way to do that.
Beyond that, if you're interested in alternate ways to structure the whole thing, one obvious variant would be to wrap Image
instead into something representing the process of constructing one, with operators merged in to transform the image being produced using something like operate
. I don't know if this would actually gain you anything, though.
In fact, I'm inclined to doubt that there're really any other ways to do this. You're writing an FFI binding to a highly imperative library and there's only so much you can do to disguise that.
I'm not sure, however, why you have an unsafe version of operate
. What practical purpose would this serve?
I'm also not sure what kind of binary operators you'd like to generalize this to--there's not much else you can do operating on ImageOperation
besides what you have here. Do you mean generalizing ImageOperation
to work on more than one mutable reference to an image? Or something involving operations on images that return something other than just IO ()
?
EDIT: Okay, let's look at how one might decompose poorMansHighPass
. Hopefully I'm correctly reading what it's doing here:
First, gaussian
is independent and can be factored out as its own operation: gauss' = ImgOp . gaussian
.
Next, subtract
can also be factored out, parameterized by an additional Image
: subtr' = ImgOp . flip subtract
.
These two are the core of the function, and they can be combined in the usual manner: poorMansHP' img = gauss' (5, 5) #> subtr' img
. The last thing needed to recover the original function is that the img
argument given to poorMansHP'
must be the same image whose clone is passed to the inner function by operate
.
First we'll explicitly unwrap the ImageOperation
and use that in the reimplementation:
poorMansHighPass img =
let (ImgOp op) = gauss' (5, 5) #> subtr' img
in do x <- clone img
op x
return x
Substitute withClone
for the use of clone
here:
poorMansHighPass img =
let (ImgOp op) = gauss' (5, 5) #> subtr' img
in withClone img $ \x -> do op x
return x
Desugar the do
block:
poorMansHighPass img =
let (ImgOp op) = gauss' (5, 5) #> subtr' img
in withClone img $ \x -> op x >> return x
...which obviously contains a reimplementation of operate
, so replace that and simplify:
poorMansHighPass img = operate (gauss' (5, 5) #> subtr' img) img
More interesting would be implementing poorMansHighPass
in a way that modifies the argument instead of the clone, which would allow it to be packaged up as an ImageOperation
itself. Possibly that's what it's supposed to be doing, and I misread your code?
Anyway, the basic structure of the refactoring would be the same, but you'd need a different composition operator--instead of applying two operators to the same input in sequence, it would need to create a clone of the input internally before recombining the results. I have a rough idea what kind of structure would make this work smoothly, which I can elaborate on if you'd like but I'll have to work through it a bit to make sure it behaves properly.
回答2:
Here is the approach I took with OpenCV code. I never saw it beyond what I needed for a couple projects I was working on, and I think this kind of approach should come with performance regression tests.
来源:https://stackoverflow.com/questions/6059505/better-interface-for-composing-destructive-operators