As was presented in the previous tutorial ezy::features::printable allows the programmer to expose a
behaviour of the underlying type. ezy's fundamental idea about features that some of them expands the
capabilities based on existing ones. For example if there a class with begin() and end(), stl algorithms
can be used with them. ezy::features::iterable provides a convenient interface for them.
Suppose that we want to work with the results of a written test in a school. Our data can be represented the following way:
#include <ezy/strong_type.h>
#include <ezy/features/iterable.h>
#include <vector>
#include <string>
// ...
using Names = ezy::strong_type<std::vector<std::string>, struct NamesTag, ezy::features::iterable>;
using Scores = ezy::strong_type<std::vector<int>, struct PointsTag, ezy::features::iterable>;
Names names{"Alice", "Bob", "Cecil", "David"};
Scores scores{10, 35, 23, 29};Names and Scores are two strong containers, and two object is created: the first contains the names, the
second one stores the scores they got for the test respectively.
names.for_each([](const std::string& name) { std::cout << name << "\n"; });for_each is a wrapper for std::for_each, so this is roughly equivalent to:
std::for_each(names.begin(), names.end(), [](const std::string& name) { std::cout << name << "\n"; });or with a simple for-loop:
for (const std::string& name : names)
{
std::cout << name << "\n";
}Maybe the latter one seems to be more familiar, until we don't make a little abstraction:
auto print_line = [](const std::string& name) { std::cout << name << "\n"; };
names.for_each(print_line); // (1) with ezy::features::iterable
std::for_each(names.begin(), names.end(), print_line); // (2)
for (const std::string& name : names)
{
print_line(name);
} // (3)If you were still not convinced, it's OK. However we will continue with the first option anyway :)
print_line("Names:");
names.for_each(print_line);Output:
Names:
Alice
Bob
Cecil
David
We should have a nicer output where names are look like as a list:
const std::string prefix = " - ";
auto prepend = [&](const std::string& s) { return prefix + s; };
names.map(prepend).for_each(print_line);prepend is a simple helper lambda which will be used for mapping the values. And what does .map do here?
it calls prepend to each element of names and returns a range of the results. std::transform would do the
same with the difference that .map() provides lazy evaluation.
names
[ [
"Alice" -> " - Alice"
"Bob" -> " - Bob"
"Cecil" -> " - Cecil"
"David" -> " - David"
] ]
This still can be expressed as a for-loop:
for (const std::string& name : names)
{
print_line(prepend(name));
}So the output is:
names:
- Alice
- Bob
- Cecil
- David
Let assume that a test is passed if has at least 25 score.
bool test_passed(int score)
{ return score >= 25; }
std::cout << "Number of passed tests: " << scores.filter(test_passed).size() << "\n";Filter returns a range with only the elements that passed the predicate and size will return the number of
its elements. It is important to note that scores doesn't changed, filter gives a lazy view of it.
After we have some intuition how can work with those algorithms, we should make some meaningful action: show the names alongside with the scores they get. There is a little helper to format this:
const auto display = [](const std::string name, int score) {
return name + " (" + ezy::to_string(score) + ")";
}; names
.zip_with(display, scores)
.map(prepend)
.for_each(print_line);zip_with is very similar to map: it calls display with every element of names, and additionally takes
elements from scores. (Its STL counterpart is std::transform invocation on two ranges)
names scores
[ [ [
"Alice" 10 display("Alice", 10) -> "Alice (10)"
"Bob" 35 display("Bob", 35) -> "Bob (35)"
"Cecil" 23 display("Cecil", 23) -> "Cecil (23)"
"David" 29 display("David", 29) -> "David (29)"
] ] ]
And since display returns a string, the following map(prepend) and for_each(print_line) can be used without any change.
- Alice (10)
- Bob (35)
- Cecil (23)
- David (29)
So far we could use zip_with(display, ...), it zipped the data into one string which is human readable, but
after that we cannot filter for scores. One could try zipping names and filtered scores, like:
// wrong
names
.zip_with(display, scores.filter(test_passed))
.map(prepend)
.for_each(print_line);
/* prints:
- Alice (35)
- Bob (23)
*/The result is simply wrong, because the first two names is used, however Bob and David has passed the test.
So first we have to zip the corresponding names and scores and then filter. To achieve this the basic .zip() function can be used, which turns every element into a tuple,
names scores
[ [ [
"Alice" 10 -> ("Alice", 10)
"Bob" 35 -> ("Bob", 35)
"Cecil" 23 -> ("Cecil", 23)
"David" 29 -> ("David", 29)
] ] ]
Logically we try to do something like:
// pseudocode
names
.zip(scores)
.filter(test_passed)
.map(display)
// ...The problem here is we have a tuple of name and score and test_passed cannot be invoked with it. So we need
to manually roll out a lambda, which selects the score from the tuple, and calls test_passed on it.
.filter([](const auto& name_with_score) {
return test_passed(std::get<1>(name_with_score));
})If you are not entirely happy with this solution, then we are on the same side. There is one experimental component which could possibly help here:
ezy::pipeit can chain functions:pipe(f, g, h)(x)is logically equivalent toh(g(f(x)))With this filter can be rewritten.filter(ezy::pipe(ezy::pick_second, test_passed))(<ezy/pipe.h>has to be included) Do you think is it better?
Unfurtunately not only filter is affected, but we cannot call display with a tuple, because it expects two
separate parameters. std::apply is the utility from the standard lib which can be used here. So one might implement a
helper lambda for it:
const auto display_with_name_and_score = [](const auto& name_with_score) {
return std::apply(display, name_with_score);
}; // not recommendedWhile it would do its job correctly, it is quite cumbersome to add a helper lambda for every possible function to be
applied. It brings a lot of noise while it only binds the function to std::apply.
Fortunately ezy::apply provides not only a polyfill for std::apply, but a curried version as well. In practice
this means that ezy::apply(display) prepares the display function to be invoked by a tuple. So from a logical
perspective, the same is equivalent:
ezy::apply(display)return names
.zip(scores)
.filter([](const auto& name_with_score) {
return test_passed(std::get<1>(name_with_score));
})
.map(ezy::apply(display));
.map(prepend)
.for_each(print_line);Next: Extending types