Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
roth_stephan_clean_c20_sustainable_software_development_patt-1.pdf
Скачиваний:
25
Добавлен:
27.03.2023
Размер:
7.26 Mб
Скачать

Chapter 7 Functional Programming

Map, Filter, and Reduce

Each serious functional programming language must provide at least three useful higher-order functions: map, filter, and reduce (called fold). Even if they have different names depending on the programming language, you can find this triumvirate in Haskell, Erlang, Clojure, JavaScript, Scala, and many other languages with functional programming capabilities. Hence, we can claim justifiably that these three higher-order functions form a very common functional programming design pattern.

It should therefore hardly surprise you that these higher-order functions are also contained in the C++ Standard Library. And maybe you will also not be surprised that we have already used some of these functions.

Let’s look at each of these functions in the following sections.

Map

Map might be the easiest to understand of the three. With the help of this higher-order function, we can apply an operator function to each single element of a list. In C++, this function is provided by the Standard Library algorithm std::transform (defined in the <algorithm> header), which you’ve seen in some previous code examples.

Filter

Filter is also pretty simply. As the name suggests, this higher-order function takes a predicate (see the section about predicates earlier in this chapter) and a list, and it removes any element from the list that does not satisfy the predicate’s condition. In C++, this function is provided by the Standard Library algorithm std::remove_if (defined in the <algorithm> header), which you’ve seen in some previous code examples.

Nevertheless, here’s another nice example of the std::remove_if filter. If you are suffering from a disease called “aibohphobia,” which is a humorous term for the

irrational fear of palindromes, you could filter out palindromes from word lists, as shown in Listing 7-25.

Listing 7-25.  Removing All Palindromes from a Vector of Words

#include <algorithm> #include <iostream>

#include <string>

324

Chapter 7 Functional Programming

#include <vector>

class IsPalindrome { public:

bool operator()(const std::string& word) const {

const auto middleOfWord = begin(word) + word.size() / 2; return std::equal(begin(word), middleOfWord, rbegin(word));

}

};

int main() {

std::vector<std::string> someWords { "dad", "hello", "radar", "vector", "deleveled", "foo", "bar", "racecar", "ROTOR", "", "C++", "aibohphobia" }; someWords.erase(std::remove_if(begin(someWords), end(someWords), IsPalindrome()),

end(someWords));

std::for_each(begin(someWords), end(someWords), [](const auto& word) { std::cout << word << ",";

}); return 0;

}

The output of this program is as follows:

hello,vector,foo,bar,C++,

Reduce (Fold)

Reduce (also called fold, collapse, or aggregate) is the most powerful of the three higher-­ order functions and might be a bit hard to understand at first glance. Reduce (fold) is

a higher-order function to get a single resultant value by applying a binary operator to a list of values. In C++, this function is provided by the Standard Library algorithm

std::accumulate (defined in the <numeric> header). Some say that std::accumulate is the most powerful algorithm in the Standard Library.

To start with a simple example, you can easily get the sum of all integers in a vector, as shown in Listing 7-26.

325

Chapter 7 Functional Programming

Listing 7-26.  Building the Sum of All Values in a Vector Using std::accumulate

#include <numeric> #include <iostream>

#include <vector>

int main() {

std::vector<int> numbers { 12, 45, -102, 33, 78, -8, 100, 2017, -110 };

const int sum = std::accumulate(begin(numbers), end(numbers), 0); std::cout << "The sum is: " << sum << std::endl;

return 0;

}

The version of std::accumulate we used does not expect an explicit binary operator in the parameter list. Using this version of the function, the sum of all values is calculated. Of course, you can provide your own binary operator, as in the example through a lambda expression in Listing 7-27.

Listing 7-27.  Finding the Highest Number in a Vector Using std::accumulate

int main() {

std::vector<int> numbers { 12, 45, -102, 33, 78, -8, 100, 2017, -110 };

const int maxValue = std::accumulate(begin(numbers), end(numbers), 0, [](const int value1, const int value2) {

return value1 > value2 ? value1 : value2; });

std::cout << "The highest number is: " << maxValue << std::endl; return 0;

}

326

Chapter 7 Functional Programming

LEFT AND RIGHT FOLD

Functional programming often distinguishes between two ways to fold a list of elements: a left fold and a right fold.

If we combine the first element with the result of recursively combining the rest, this is called a right fold. Instead, if we combine the result of recursively combining all elements but the last one with the last element, this operation is called a left fold.

If, for example, we take a list of values that are to be folded with a + operator to a sum, the parentheses are as follows for a left fold operation: ((A + B) + C) + D. Instead, with a right fold, the parentheses would be set like this: A + (B + (C + D)). In the case of a simple associative + operation, the result does not change whether it is formed with a left fold or a right fold. But in the case of non-associative binary functions, the order in which the elements are combined may influence the final result’s value.

Also in C++, we can distinguish between a left fold and a right fold. If we use std::accumulate with normal iterators, we get a left fold:

std::accumulate(begin, end, init_value, binary_operator)

Instead, if we use std::accumulate with a reverse iterator, we get a right fold:

std::accumulate(rbegin, rend, init_value, binary_operator)

Fold Expressions in C++17

Starting with C++17, the language has gained an interesting new feature called fold expressions. Fold expressions are implemented as so-called variadic templates (available since C++11); that is, as templates that can take a variable number of arguments in a type-safe way. This arbitrary number of arguments is held in a so-called parameter pack.

What has been added with C++17 is the possibility to reduce the parameter pack directly with the help of a binary operator, that is, to perform a folding. The general syntax of C++17 fold expressions are as follows:

327

Chapter 7 Functional Programming

 

 

( ... operator parampack )

// left fold

( parampack operator ... )

// right

fold

( initvalue operator ... operator parampack )

// left fold with an init

 

value

 

( parampack operator ... operator initvalue )

// right

fold with an init

 

value

 

Listing 7-28 shows an example, a left fold with an init value.

Listing 7-28.  An example of a Left Fold

#include <iostream>

template<typename... PACK>

int subtractFold(int minuend, PACK... subtrahends) { return (minuend - ... - subtrahends);

}

int main() {

const int result = subtractFold(1000, 55, 12, 333, 1, 12); std::cout << "The result is: " << result << std::endl; return 0;

}

Note that a right fold cannot be used in this case due to the lack of associativity of operatort. Fold expressions are supported for 32 operators, including logical operators like ==, &&, and ||.

Listing 7-29 shows another example, which tests that a parameter pack contains at least one even number.

Listing 7-29.  Checking Whether a Parameter Pack Contains an Even Value

#include <iostream>

template <typename... TYPE>

bool containsEvenValue(const TYPE&... argument) { return ((argument % 2 == 0) || ...);

}

328