Book Image

Hands-On Functional Programming with C++

By : Alexandru Bolboaca
Book Image

Hands-On Functional Programming with C++

By: Alexandru Bolboaca

Overview of this book

Functional programming enables you to divide your software into smaller, reusable components that are easy to write, debug, and maintain. Combined with the power of C++, you can develop scalable and functional applications for modern software requirements. This book will help you discover the functional features in C++ 17 and C++ 20 to build enterprise-level applications. Starting with the fundamental building blocks of functional programming and how to use them in C++, you’ll explore functions, currying, and lambdas. As you advance, you’ll learn how to improve cohesion and delve into test-driven development, which will enable you in designing better software. In addition to this, the book covers architectural patterns such as event sourcing to help you get to grips with the importance of immutability for data storage. You’ll even understand how to “think in functions” and implement design patterns in a functional way. By the end of this book, you’ll be able to write faster and cleaner production code in C++ with the help of functional programming.
Table of Contents (23 chapters)
Free Chapter
1
Section 1: Functional Building Blocks in C++
7
Section 2: Design with Functions
12
Section 3: Reaping the Benefits of Functional Programming
17
Section 4: The Present and Future of Functional Programming in C++

Structured loops versus functional loops

It's hardly a surprise that one of the first things that we learn as programmers is how to write a loop. One of my first loops in C++ was printing the numbers from 1 to 10:

for(int i = 0; i< 10; ++i){
cout << i << endl;
}

As a curious programmer, I took this syntax for granted, went over its peculiarities and complications, and just used it. Looking back, I realize that there are a few unusual things about this construct. First, why start with 0? I've been told it's a convention, due to historical reasons. Then, the for loop has three statements—an initialization, a condition, and an increment. This sounds slightly too complicated for what we're trying to achieve. Finally, the end condition forced me into more off-by-one errors than I'd like to admit.

At this point, you will realize that STL allows you to use iterators when looping over collections:

for (list<int>::iterator it = aList.begin(); it != aList.end(); ++it)
cout << *it << endl;

This is definitely better than the for loop using a cursor. It avoids off-by-one errors and there are no 0 convention shenanigans. There's still a lot of ceremony around the operation, however. Even worse is that the loop tends to grow as the complexity of the program grows.

There's an easy way to show this symptom. Let's take a look back at the first problems that I've solved using loops.

Let's consider a vector of integers and compute their sum; the naive implementation will be as follows:

int sumWithUsualLoop(const vector<int>& numbers){
int sum = 0;
for(auto iterator = numbers.begin(); iterator < numbers.end();
++iterator){
sum += *iterator;
}
return sum;
}

If only production code was so simple! Instead, the moment we implement this code, we'll get a new requirement. We now need to sum only the even numbers from the vector. Hmm, that's easy enough, right? Let's take a look at the following code:

int sumOfEvenNumbersWithUsualLoop(const vector<int>& numbers){
int sum = 0;
for(auto iterator = numbers.begin(); iterator<numbers.end();
++iterator){
int number = *iterator;
if (number % 2 == 0) sum+= number;
}
return sum;
}

If you thought this is the end, it's not. We now require three sums for the same vector—one of the even numbers, one of the odd numbers, and one of the total. Let's now add some more code, as follows:

struct Sums{
Sums(): evenSum(0), oddSum(0), total(0){}
int evenSum;
int oddSum;
int total;
};

const Sums sums(const vector<int>& numbers){
Sums theTotals;
for(auto iterator = numbers.begin(); iterator<numbers.end();
++iterator){
int number = *iterator;
if(number % 2 == 0) theTotals.evenSum += number;
if(number %2 != 0) theTotals.oddSum += number;
theTotals.total += number;
}
return theTotals;
}

Our loop, which initially started relatively simple, has become more and more complex. When I first started professional programming, we used to blame users and clients who couldn't make up their minds about the perfect feature and give us the final, frozen requirements. That's rarely possible in reality, however; our customers learn new things every day from the interaction of users with the programs we write. It's up to us to make this code clear, and it's possible with functional loops.

Years later, I learned Groovy. A Java virtual machine-based programming language, Groovy focuses on making the job of programmers easier by helping them to write less code and avoid common errors. Here's how you could write the previous code in Groovy:

def isEven(value){return value %2 == 0}
def isOdd(value){return value %2 == 1}
def sums(numbers){
return [
evenSum: numbers.filter(isEven).sum(),
oddSum: numbers.filter(isOdd).sum(),
total: numbers.sum()
]
}

Let's compare the two for a moment. There's no loop. The code is extremely clear. There's no way to make off-by-one errors. There's no counter, so, therefore, there is no starting from 0 weirdness. Additionally, there's no scaffolding around it—I just write what I want to achieve, and a trained reader can easily understand it.

While the C++ version is more verbose, it allows us to achieve the same goals:

const Sums sumsWithFunctionalLoops(const vector<int>& numbers){
Sums theTotals;
vector<int> evenNumbers;
copy_if(numbers.begin(), numbers.end(),
back_inserter(evenNumbers), isEven);
theTotals.evenSum = accumulate(evenNumbers.begin(),
evenNumbers.end(), 0);

vector<int> oddNumbers;
copy_if(numbers.begin(), numbers.end(), back_inserter(oddNumbers),
isOdd);
theTotals.oddSum= accumulate(oddNumbers.begin(), oddNumbers.end(),
0);

theTotals.total = accumulate(numbers.begin(), numbers.end(), 0);

return theTotals;
}

There's still a lot of ceremony though, and too much code similarity. So, let's get rid of it, as follows:

template<class UnaryPredicate>
const vector<int> filter(const vector<int>& input, UnaryPredicate filterFunction){
vector<int> filtered;
copy_if(input.begin(), input.end(), back_inserter(filtered),
filterFunction);
return filtered;
}

const int sum(const vector<int>& input){
return accumulate(input.begin(), input.end(), 0);
}

const Sums sumsWithFunctionalLoopsSimplified(const vector<int>& numbers){
Sums theTotals(
sum(filter(numbers, isEven)),
sum(filter(numbers, isOdd)),
sum(numbers)
);
return theTotals;
}

We've just replaced a complex for loop with a number of simpler, more readable, and composable functions.

So, is this code better? Well, that depends on your definition of better. I like to think of any implementation in terms of advantages and disadvantages. The advantages of functional loops are simplicity, readability, reduced code duplication, and composability. Are there any disadvantages? Well, our initial for loop only requires one pass through the vector, while our current implementation requires three passes. This can be a burden for very large collections, or when response time and memory usage are very important. This is definitely worth discussing, and we will examine it in more detail in Chapter 10, Performance Optimization, which is focused solely on performance optimization for functional programming. For now, I recommend that you focus on understanding the new tool of functional programming.

In order to do that, we need to revisit immutability.