Processing single data items
The first issue to be addressed is how to express data flow operations in
C++. We could use functions calls to execute the operations in a pipe line,
but this quickly becomes unwieldy, and unclear especially if the functions
have more than one argument.
results = Normalise(Smooth(Read("my.data")));
A clearer version of this can be written using a simple template
function on the >>
operator. Processing a single piece of data
through a pipe line can be achieved quite easily. The simplest kind of
processing can be done with a few templates, the first of which is
defined as follows.
template <class InT,class OutT>
OutT operator>>(const InT & in,OutT (*func)(const InT &))
{ return func(in); }
An example of the use of this template follows. The
Read(...)
function is assumed to return the data we wish
to process; this data is processed by the Normalise
and
Smooth
functions and the result of the computation is
stored in `results'.
results = Read("my.data") >> Normalise >> Smooth;
To complete the path we can add a template operator to copy the output
into a result variable. So we can complete the pipe in a way
consistent with the rest of the notation. This may seem unnecessary,
but it keeps the syntax consistent with operations that will be
introduced in the next section. The definition of the new templated
>>
operator is given below.
template <class InT,class OutT>
OutT operator>>(const InT &in,OutT &out)
{ return out = in; }
This allows us to write the previous example as follows.
Read("my.data") >> Normalise >> Smooth >> results;
The data flow is now completely left to right. This will be kept true
in all the following examples and in the diagrams that will be
introduced later. Often we want to setup parameters for a process, or
a process has state which is preserved between operations. For this
it is more convenient to use a class to represent the processing
operation.
To allow a set of templates to be written which will handle these
processes automatically it is assumed that each class used for
processing data has a method Apply
which transforms the
input data to the output. To allow full template instantiation on
parameters of the pipe operator >>
it is easier if all such
processes are derived from a single templated base class. In the RAVL
system this class is called DPProcessC<InT,OutT>
and has an abstract virtual apply method called OutT Apply(const
InT &dat)
.
Again we can add a template function which will call the apply operator in
the appropriate place in a stream.
template <class InT,class OutT>
OutT operator>>(const InT &in,const DPProcessC<InT,OutT> & proc)
{ return const_cast<DPProcessC<InT,OutT> &> proc.Apply(in); }
A typical application for this kind of process is convolution, where we
wish to specify a kernel.
Read("file") >> ConvolveC(kernel) >> Normalise >> result;
So far the examples given are all semantic sugar. They allow a few
operations written in a slightly simpler way, but do not provide any
real extra functionality. In RAVL these piping elements are declared
within a namespace RavlComposeSingleN
.
Normal functions: