So you have an Object Oriented library but yet want to be able to use F#’s functional pipelining feature to design expressive data processing workflows. How do you go about it?
First, lets set a goal. Some low hanging fruit so to speak. Let’s pretend we have a set of images we want to load, resize, intensify and save as Png files for later use in an online image gallery.
Image Processing In C#
For reference, this would look something like the following using our DotImage toolkit in C#:
public void ProcessImage(string fromfile, string tofile)
{
ImageCommand[] commands = new ImageCommand[] {
new ChangePixelFormatCommand( PixelFormat.Pixel8bppIndexed ),
new ResampleCommand( new Size( 800, 600) ),
new IntensifyCommand( 50.0 )
};
AtalaImage currentImage = new AtalaImage(fromfile);
foreach (var command in commands)
{
currentImage = command.Apply(currentImage).Image;
}
currentImage.Save(tofile, new PngEncoder(), null);
}
It’s a testament to the skill of our Senior Architect that this task is as simple as it is in C#.
Note: our image representation is IDisposable and for optimal performance should immediately be disposed when done being used. I’ll be covering how to leverage F#’s type system to handle this in a later post.
Now, in F#
In comparison, this is how I envision this same process using F#’s functional pipelining style:
let processImage infile outfile =
Image.fromFile infile
|> Image.changePixelFormat Image.PixelFormat.Pixel8bppIndexed
|> Image.resample 800 600
|> Image.intensify 50.0
|> Image.toPngFile outfile
These pipelined functions are so easy on the eyes. It’s immediately obvious what’s going on here. Unfortunately, it can be rather difficult to use pipelining with non function constructs.
Wrapping an Object Oriented Library for Pipelining
To bring this seamless integration with F#, we must first wrap these Object Oriented classes so that they can be used in a functional way. This is a rather simple task:
namespace Atalasoft.FSharp
open System.Drawing
open Atalasoft.Imaging
open Atalasoft.Imaging.Codec
open Atalasoft.Imaging.ImageProcessing
open Atalasoft.Imaging.ImageProcessing.Filters
module Image =
type image = Atalasoft.Imaging.AtalaImage
type PixelFormat = Atalasoft.Imaging.PixelFormat
let fromFile (filename: string) =
new image( filename )
let toPngFile (filename: string) (img: image) =
img.Save( filename, new PngEncoder(), null) |> ignore
let resample (width: int) (height: int) (img: image) =
let newSize = new Size( width, height ) in
let cmd = new ResampleCommand( newSize ) in
cmd.Apply(img).Image
let intensify (magnitude: double) (img: image) =
let cmd = new IntensifyCommand( magnitude ) in
cmd.Apply(img).Image
let changePixelFormat (pf: PixelFormat) (img: image) =
let changer = new AtalaPixelFormatChanger() in
changer.ChangePixelFormat(img, pf, null)
So, in this way, we can define modules which will hide our object oriented interfaces. However, let’s take a deeper look. There are a some important things to keep in mind when designing functions for pipelining.
First, observe that the module definition encapsulates all of our pipelining functions and mandates how they will be accessed later. It is good design practice to define all pipelining functions for the same type within one module. This module should have the same name as the type but with the first letter capitalized.
Second, notice that we use type abbreviations to create local versions of the AtalaImage and PixelFormat types. This makes our library code easier to read and allows us to use the F# lowercase type naming style. Even more importantly, by defining all exposed types in this way the consumer of this module will not need to open any namespaces from the assemblies we are wrapping.
Third, see how image is the last parameter to each function? To be able to pipeline into a function its final parameter must be of the to-be-pipelined type.
Note: This is not strictly true. If you wish to pipeline into a function and then return a function with that value curried in, it can have additional parameters which will be filled in later. However, that’s a topic best left for another time.
Finally, note that the return type of each of the intermediate image processing functions is also image. This ensures that the output can be pipelined directly into the next function.
Our F# Wrapper in Action
Let’s highlight the contents of this fsx file, hit alt-enter, and give it a whirl in the F# Interactive Window:
#r "Atalasoft.dotImage"
#r "Atalasoft.dotImage.Lib"
#r "Atalasoft.Shared"
#load "Image.fs"
open Atalasoft.FSharp
let processImage infile outfile =
Image.fromFile infile
|> Image.changePixelFormat Image.PixelFormat.Pixel32bppBgr
|> Image.resample 400 300
|> Image.intensify 70.0
|> Image.toPngFile outfile
Finally, in F# Interactive we can simply type:
> processImage @"C:\temp\Water lilies.jpg" @"C:\temp\Water lilies.png";;
val it : unit = ()
and we have our processed image:
Conclusion
Of course, this implementation leaves much to be desired. It hides the vast majority of the available functionality in our underlying library. Taken to it’s natural end it leaves us with the unfortunate task of wrapping our entire library one command at a time, which is hardly an appealing prospect.
What if you wish to leverage existing objects without wrapping each individually? Well, we will explore that next time.