Overly Functional C++: The BenFolds Five

Ben Lovy - Oct 3 '19 - - Dev Community

Two-Part Series, Chapter 3

What a plot twist! I made almost no effort to avoid the pun and I'm only a little sorry.

Despite being a trilogy now, this post stands alone if you're familiar with the concept of the fold or reduce higher-order function.

In part 2 of yesterday's minddump, I documented my first stab at a specific fold in C++. I had gotten stuck, though, in trying to make it generic.

I've said it before and I'll say it again: ask DEV stuff, they know things. Thanks to @curtisfenner and @markboer I was able to write the intended generic fold function I had set out to write initially with almost no modification. This newly-generic fold function is a building block, and if you give a nerd a fold...

Five Library Functions

BenFolds is a class with five static member functions. It defines BenFolds::fold() as a generic version of the code from part 2, and then defines or, any, elem, and map in terms of this fold. I'll walk through each, or you can grab the gist. This gist compiled and executed successfully for me using g++ -std=c++11 -o benfolds benfolds.cpp on g++ 9.2.0.

Fold

This is the only definition with any substance, the rest will all specify parameters to run through this fold:

template <typename T, typename R, typename BinOp>
static R fold(std::vector<T> elems, R acc, BinOp func)
{
    int length = elems.size();
    if (length == 0)
        {
            // base case - empty list
            // Sub-problem is trivial - we're already done.  The passed accumulator holds the result
            return acc;
        }
        else
        {
            // Calculation is not complete - sub-problem is also trivial
            // Call function on accumulator using first element in vector as operand
            R newAcc = func(acc, elems[0]);
            // Create a new input from the second element onward of the current input
            std::vector<T> newInput(elems.begin() + 1, elems.end());
            // Recur with rest of list and new accumulator
            return fold(newInput, newAcc, func);
        }
}
Enter fullscreen mode Exit fullscreen mode

This implementation is identical to the solution from part 2 apart from the parameterized types. The other four "backup" functions are just specific cases of this fold.

As @curtisfenner pointed out, this naive implementation is needlessly expensive. The recursion is in tail position, which the compiler does optimize for, but each call is allocating a brand new vector to swap into the stack frame. If you needed to optimize this further, you could consider instead passing ever-smaller subslice references to the same vector, or even mutating the original vector in place instead.

foldOr

The simplest application just takes a list of booleans and tells you if any of them are true. The T in BenFolds::fold() is bool:

static bool foldOr(std::vector<bool> bools)
{
    return fold<bool>(bools, false, [](bool acc, bool curr) { return acc || curr; });
}
Enter fullscreen mode Exit fullscreen mode

The initial accumulator is false. We call || on this accumulator against each element of the passed collection. At the end, if any element was true, the accumulator flipped to true and stuck. Otherwise, nothing was found it's still false.

To test, I added a quick throwaway to main():

vector<bool> bools = {false, false, false, true, false};
cout << "Testing foldOr...\nThis should be true: " << bf.foldOr(bools) << "\n";
Enter fullscreen mode Exit fullscreen mode

foldAny

The any function is just a generalization of or:

template <typename T, typename Predicate>
static bool foldAny(std::vector<T> elems, Predicate p)
{
    return fold(elems, false, [p](bool acc, T curr) { return acc || p(curr); });
}
Enter fullscreen mode Exit fullscreen mode

It's really almost the same thing. We're still calling || on the accumulator and this element, but we're passing the element through some other predicate p before checking for truth.

For example, see if the inputs has any even numbers (it should) or anything greater than 10 (it shouldn't):

cout << "Testing foldAny...\nAre there even elements in the set: " << bf.foldAny(nums, [](int n) { return n % 2 == 0; }) << "\n";
cout << "Are there elements greater than 10: " << bf.foldAny(nums, [](int n) { return n > 10; }) << "\n";
Enter fullscreen mode Exit fullscreen mode

foldElem

The elem function checks if an element exists in a collection, which is a specialization of any so we can reuse that definition:

template <typename T>
static bool foldElem(std::vector<T> elems, T elem)
{
    return foldAny(elems, [elem](T curr) { return elem == curr; });
}
Enter fullscreen mode Exit fullscreen mode

It calls foldAny defining the predicate as equality against a specific element. We can test by checking for a specific number, no need to pass a lambda:

cout << "Testing foldElem...\nIs the number 2 present in the set: " << bf.foldElem(nums, 2) << "\n";
cout << "Is the number 8 present in the set: " << bf.foldElem(nums, 8) << "\n";
Enter fullscreen mode Exit fullscreen mode

foldMap

We can also define a map function from this fold that builds the target vector in the accumulator:

template <typename T, typename Op>
static std::vector<T> foldMap(std::vector<T> elems, Op func)
{
    return fold(elems, std::vector<T>(), [func](std::vector<T> acc, T curr) {
        acc.push_back(func(curr));
        return acc;
    });
}
Enter fullscreen mode Exit fullscreen mode

I tested this one by doubling each element in the input:

cout << "Testing foldMap...\nHere's each element doubled: " << bf.foldMap(nums, [](int elem) { return elem * 2; }) << "\n";
Enter fullscreen mode Exit fullscreen mode

Running through all the tests as defined yields this output:

Set: [0, 1, 2, 3, 4]
Testing fold...
Sum: 10
Testing foldOr...
This should be true: 1
Testing foldAny...
Are there even elements in the set: 1
Are there elements greater than 10: 0
Testing foldElem...
Is the number 2 present in the set: 1
Is the number 8 present in the set: 0
Testing foldMap...
Here's each element doubled: [0, 2, 4, 6, 8]
Enter fullscreen mode Exit fullscreen mode

Good enough

Photo by Oscar Keys on Unsplash

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .