Let’s imagine you start working on a new project for your customer. The mission: Build a machine that is able to estimate the amount of beer drunk by a crowd by processing an image of the crowd. You start thinking about how to approach the problem and you notice that it can be easily split into certain stages.
- Stage 1, Shape Detection: Detect low level features like shapes. Circles, Polygons, etc
- Stage 2, Feature Detection: Detect higher level features like: Faces, Arms, Shoes, etc based on previously found shapes
- Stage 3, Person Detection: Detect individual persons in the image based on previously found features
- Stage 4, Measure: Use some fancy AI algorithm to calculate how drunk each person is and estimate the amount of beer necessary for the whole crowd
So far so good! You decided to model this thing as a pipeline with those 4 stages. So what are you putting into this pipeline?
What about: Creating a Request and inherit from it?
Let’s jump right in. Here is the code:
Sooooo 🧐 Waait a second…
What do you mean with preparedImage???
Here is the thing: You are using a special framework, that is not able to work with the images of your camera directly. This means you need to provide the correct type of image by converting it. This example will use ‘Mat’ to represent this image types.
So after modelling your Request type like this you start to notice certain things:
1. Every Request shares some attributes (image, timestamp, measureSettings)
2. All higher stage Request are sharing preparedImage and preparedScaledImage
3. The source image itself is not really used anymore after converting it to ‘Mat’
So what is the solution? Creating a more complex inheritance model to share attributes? Sure! But how can you close the source image after converting it? Should you separate the initial Request from all the other Request types?
Oh dear 🙁. This seams to get messy pretty fast, even with this almost trivial example. At this point you start feeling frustrated. You could do it, but you don’t really like the idea. There has to be a better way ¯\_(ツ)_/¯
What about: Phantom Types
At this moment you start thinking about a more unconventional way of handling read rights of your request object. You wonder:
What if you would have a generic type parameter which represents the stage of the request. And what if this parameter magically could have control over what you can read from this object? 🤔
What is happening in the gif above?
One can see how the read rights of certain values change by changing the phantom type of your request object. An object of type Request<Stage.Pending> only provides the image as ‘Image’ instead of ‘Mat’ and does not provide any scaledImage or shapes. An object of type Request<Stage.FeatureDetection> on the other hand cannot provide an image of type ‘Image’ but can provide shapes and image + scaledImage as ‘Mat’
But how can this be implemented and how can this be easier than just managing multiple classes?
- Step 1: Implement the Request Class.
Okay. Now you have an object that is able to hold a reference to everything the pipeline could need. It contains the generic type param T which does not do anything for us and everything is private. So we cannot do anything with it. What is the point?
- Step 2: Implement ‘Read-Right-Interfaces’
It is really as simple as creating an empty interface for every attribute you want to manage.
- Step 3: Implement companion access for your values.
Now you have a public method to access you values for certain Phantom Types. Here is one downside of this design: You need to model your transitions (how to get a Request into a certain Stage) correctly, otherwise you would get this ‘StageException’ at runtime. You also need to find a way to prevent the construction of a Request in arbitrary Stage. Both problems are relatively easy but won’t be covered by this blog post!
- Step 4: Create convenience extensions
You obviously don’t want to use this companion functions directly. You obviously want a familiar developer experience when working in your pipeline. The difference between straight forward read rights and ‘Phantom-Read-Rights’ won’t be really noticeable with this extensions! 😋
- Step 5: Define your stages
You obviously want to have a Phantom Type for every stage of your pipeline. Every of those stages will be able to read the ‘MeasureSettings’ and the ‘Timestamp’.
A Stage then just extends the specific read rights that it want’s to publish and… 🎉 Yippieh! Phantom Read Rights 🎊
The ‘Phantom Read Rights’ technique is not suitable for a broad set of problems, but it can be extremely useful for certain problems. This technique is used by QuickBird Studios for a very special customer in a very special project and has been proven very useful when modelling a pipeline.
If you are further interested in the way we at QuickBird Studios approach problems, check out our blog 😋