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.
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
Monoidinstance:So that’s at least one standard name you could use.
Further, the above
Monoidis actually a straightforward result of the properties of two other well-known types:The
Applicativeand/orMonadinstance for(->) adescribes 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 theReadermonad.The
Monadinstance 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
ImageOperations) and imagining that the implied monoid forIO ()is an actual instance, we could write: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
ReaderTandSTM.That handles combining operations. To actually run an operation, you need an
Imageand 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 theMonoidis, there’s really nothing you can’t cram into anImageOperationbefore actually running it. Generating the clone is presumably anIOoperation and is what I assume is going on inoperate–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
Imageinstead into something representing the process of constructing one, with operators merged in to transform the image being produced using something likeoperate. 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
ImageOperationbesides what you have here. Do you mean generalizingImageOperationto work on more than one mutable reference to an image? Or something involving operations on images that return something other than justIO ()?EDIT: Okay, let’s look at how one might decompose
poorMansHighPass. Hopefully I’m correctly reading what it’s doing here:First,
gaussianis independent and can be factored out as its own operation:gauss' = ImgOp . gaussian.Next,
subtractcan also be factored out, parameterized by an additionalImage: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 theimgargument given topoorMansHP'must be the same image whose clone is passed to the inner function byoperate.First we’ll explicitly unwrap the
ImageOperationand use that in the reimplementation:Substitute
withClonefor the use ofclonehere:Desugar the
doblock:…which obviously contains a reimplementation of
operate, so replace that and simplify:More interesting would be implementing
poorMansHighPassin a way that modifies the argument instead of the clone, which would allow it to be packaged up as anImageOperationitself. 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.