Chain Functor

From time to time you need to write this:

auto someRes = funcA(
   funcB(funcC(someVar))
);

Which is a really nice and elegant functional composition. Now, if we wanted to exract this pattern and encapsulate this in a one function, we could:

  • Write a function that does it
  • Use chain functor from templatious library

The previous expression could be replaced as follows:

auto chain = SF::chainFunctor(funcC,funcB,funcA);
auto someRes = chain(someVar);

First one is called first, second called 2nd, and third called third.

On the other hand, functions might take same stateful value and modify it one by one like so:

void funcA(int& a) { a*=2; }
void funcB(int& a) { a*=3; }
void funcC(int& a) { a*=5; }

...
int mul = 1;
funcA(mul);
funcB(mul);
funcC(mul);

And there's stateful chain functor version for that also:

auto chain = SF::chainFunctor<true>(funcA,funcB,funcC);
int mul = 1;
chain(mul);

We specified boolean template parameter to true, this indicates whether this is stateful version. This defaults to false, which results in functional chain shown previously, but since it is stateful here it will be called on same stateful arguments passed for every invocation of separate functions.

Now, this is the last and possibly the most useful property of chain functor - pairs. Sometimes we might want to encode doable and undoable actions. Let's make a simple but useful example - string encryption and compression.

std::vector<char>
encrypt(const std::vector<char>& msg);
std::vector<char>
decrypt(const std::vector<char>& msg);

std::vector<char>
compress(const std::vector<char>& bytes);
std::vector<char>
decompress(const std::vector<char>& bytes);

So these are the four actions, that will need to be done and undone in specific order, calling specific functions or nothing will make sense. What was encrypted with the encrypt function can be decrypted with decrypt function and what is compressed with compress function can be decompressed with decompress function. We can encapsulate the exact order of operations into chain functor.

auto cf = SF::chainFunctor(
    SF::functorPair(encrypt,decrypt),
    SF::functorPair(compress,decompress)
);

Now, when creating chain functor, we pass functor pairs, with their respective do and undo actions and we can use it to do and undo stuff. Here's an example usage:

auto encryptedAndCompressed = cf(someBytes);
auto backToPrevious = cf.doBwd(encryptedAndCompressed);

Assuming that our encryption and compression functions do what we expect, we should receive the same bytes in backToPrevious variable.

Method doBwd is available in chain functor only and ONLY if all the variables added to chain functor were functor pairs (otherwise it is compile time error).

If we'd like to receive separate functor that undoes the do action, we can do so:

auto doAct = cf.getDo();
auto undo = cf.getUndo();

auto fwd = doAct(someBytes);
auto bwd = undo(fwd);

This was functional variant, however, stateful also works just as previously:

// modify the same buffer
void encrypt(std::vector<char>& v);
void decrypt(std::vector<char>& v);

void compress(std::vector<char>& v);
void decompress(std::vector<char>& v);

...

auto chain = SF::chainFunctor<true>(
    SF::functorPair(encrypt,decrypt),
    SF::functorPair(compress,decompress)
);

// same as () operator
chain.doFwd(someVector);
// now some vector is encrypted and compressed

chain.doBwd(someVector);
// now some vector is back to what it was

We can also invert chain functors, if there was ever such need:

auto invert = chain.reverse();

// now, invert's doBwd does the same
// as chain's doFwd
invert.doBwd(someVector);
// some vector encrypted and compressed

// doFwd now does what chain's doBwd does
invert.doFwd(someVector);
// some vector back to original state