My question is the following: Are there any design patterns for representing chainable functions that are for the problem described below?
High-Level Decription of the Process
I'm currently building an image-processing server, whose functionality is exposed via a web API.
Clients first authenticate, then request that one or more analyses be performed by the server. Once a client has authenticated and requested his analysis stack, he streams his video data to the analysis server.
On the server side, the
Stream class is used to represent the flow of data. It's the first-class object around which the pipeline is built, and it can be thought of as an ordered FIFO queue of sorts. Frames are pushed, in order of arrival, onto the
Stream instance, and each stream may optionally map a function onto its data. Results from a mapped function are projected onto a new
# input_stream is asynchronously updated with incoming frames stream = input_stream.map(do_sobel_filter) # apply Sobel filter to each incoming frame stream2 = stream.window(fn, 10) # apply function to a sliding window of 10 frames
An important side-note is that our use of
Stream objects allows us to apply functions either to individual frames or to groups of frames. This is useful as some analyses require context.
Requirements & Constraints
The above bold text is particularly important; A given client will be authorized only to perform a subset of the image analyses offered by our system. This in turn makes it critical that we be able to dynamically compose the image-processing pipeline -- we don't want to perform expensive computations only to throw away the result.
Further complicating the matter is the fact that almost all of the analyses we offer have some prerequisites, that is, operation Oi may depend on the result of operation Oi-n. Our system needs to resolve these dependencies and run each operation exactly once.
Finally, the chaining/composition of operations must be associative. This is because we'd like to be able to compose reusable higher-level computations from lower-level atomic computations. For instance, consider the sequence of operations
x -> y -> z. We would like to be able to assign the ordered chain
x->y->z to a variable, thus
CMP1 = x->y->z. Similarly,
CMP2 is the result of chaining operations
a->b->c. Ultimately, we would like to be able to do the equivalent of
CMP3 = CMP1 -> CMP2, and have this be strictly equivalent to
CMP3 = x->y->z->a->b->c (associativity).
To summarize, here are the requirements for my image analysis function objects:
- Functions must be composeable, meaning that it should be possible to manipulate a first-class object that represents a series of sub-computations.
- Ideally, functions should resolve dependencies, meaning that doing
A -> Bshould implicitly evaluate to
A -> X -> Cif
Cdepends on both
X. This may be worthy of it's own question, and so should be treated as a soft requirement.
- Chaining of operations should be associative, and composed functions should also be chainable. The idea is to have a reusable pattern for representing increasingly complex pipelines.
Are there any well-tested patterns for achieving such a result?
1) In response to @Giorgio's questions, my (admittedly ambiguous) notation serves to distinguish between the temporal order of frames and the order of operations. Below is an example of how an operation can depend on its preceding counterparts:
FindFace -> FindEyes -> SegmentEye -> MeasurePupilDiameter
Here we can see that each function after
FindFace depends on the cumulative result of the preceding functions. Now consider:
FindFace -> FindEyes -> SegmentEye -> GetIrisColorHistogram
This second example demonstrates how there is often a common core of operations that needs to be performed across analyses. When composing operations, I'd like to do steps 1 - 3 only once, and then apply
GetIrisColorHistogram from there.
Intuitively, it seems like the right way to do this would be to assign a dictionary to my
Frame object and fill it with features as they're computed -- a memoize pattern, if you will. From there, however, it's unclear to me how I should go about abstracting all the boilerplate code pertaining to (a) checking if a prerequisite feature has already been computed and (b) automatically calling the appropriate function if it has not.