A wrapper for the C++ workhorses std::vector, std::set and std::map geared towards functional programming and fluent APIs. This project is heavily influenced and inspired by C# and Swift.
The primary focus of this library is
- readability at the call site ("make it work, make it right, make it fast")
- surfacing existing algorithms from the standard library, and lowering the barrier for their extended usage
- elimination of vector index operations
- encapsulation of the iterator madness
- removal of manual for-loops
- Compilation (Cmake)
- Error handling
- Functional vector usage (fcpp::vector)
- Functional set usage (fcpp::set)
- Functional map usage (fcpp::map)
- CMake >= 3.14
- C++11
An out-of-source build strategy is used. All following examples assume an output build folder named build. If no additional argument is passed to CMake, C++11 is used. Otherwise, you can pass -DCMAKE_CXX_STANDARD=17 and it will use C++17 for example.
cd functional_cppcmake -S . -B build -G XcodeThen open the generated functional_cpp.xcodeproj in the build folder.
cd functional_cppcmake -S . -B buildcmake --build buildbuild/tests/unit_testsAssuming you have installed Homebrew, you can then use the gcc and g++ compilers by doing the following (this example uses version gcc 11)
cd functional_cppcmake \ -S . \ -B build \ -DCMAKE_C_COMPILER=/opt/homebrew/Cellar/gcc/11.2.0/bin/gcc-11 \ -DCMAKE_CXX_COMPILER=/opt/homebrew/Cellar/gcc/11.2.0/bin/g++-11cmake --build buildbuild/tests/unit_testscd functional_cppcmake -S . -B buildcmake --build buildbuild/tests/unit_testscd functional_cppcmake -S . -B buildThen open the generated functional_cpp.sln in the build folder.
Operations with a precondition (for example subscripting with operator[], replace_range_at, or zip on containers of unequal size) validate that precondition at runtime. If it is violated, the program is terminated immediately via std::abort().
Unlike the standard library's assert, these checks are always active and behave identically in debug and release builds (they are not disabled by NDEBUG), so an out-of-bounds access fails fast in production instead of becoming silent undefined behavior.
If you have a performance-critical section whose inputs are already known to be valid, you can compile the checks out by defining FCPP_NO_PRECONDITION_CHECKS:
cmake -S . -B build -DCMAKE_CXX_FLAGS="-DFCPP_NO_PRECONDITION_CHECKS"With the checks disabled, violating a precondition is undefined behavior, exactly like the underlying std::vector/std::set/std::map.
#include"vector.h"// instead of <vector>const fcpp::vector<int> numbers({1, 4, 2, 5, 8, 3, 1, 7, 1});
// contains only 1, 2, 3, 4, 5, 7, 8const fcpp::set<int> unique_numbers = numbers.distinct();#include"vector.h"// instead of <vector>structperson {
person(int age, std::string name)
: age(age), name(std::move(name))
{}
int age;
std::string name;
std::size_thash() const {
// a clever implementation of hash // ...
}
booloperator< (const person& other) const {
returnhash() < other.hash();
}
};
// ...// the employees' agesconst fcpp::vector<int> ages({32, 45, 37, 23});
// the employees' namesconst fcpp::vector<std::string> names({"Jake", "Anna", "Kate", "Bob"});
constauto employees_below_40 = ages
// zip two vectors for simultaneous processing
.zip(names)
// apply the functional map algorithm (transform from one type to another)
.map<person>([](const std::pair<int, std::string>& pair) { returnperson(pair.first, pair.second);
})
// filter the elements using a local function (lambda)
.filter([](const person& p) {
return p.age < 40;
})
// sort according to custom predicate
.sort([](const person& person1, const person& person2) {
return person1.age < person2.age;
});
/* prints the following: Bob is 23 years old. Jake is 32 years old. Kate is 37 years old.*/
employees_below_40.for_each([](const person& p) {
std::cout << p.name << " is " << p.age << " years old." << std::endl;
});
// total_age = 92constauto total_age = employees_below_40.reduce(0, [](constint& partial_sum, const person& p){
return partial_sum + p.age;
});Lazy vectors are useful when chaining multiple operations over a large vector. A regular map().filter().reduce() style chain creates intermediate vectors and iterates once per algorithm. Calling .lazy() stores the following operations and executes them only when a terminal operation is called, such as get() or reduce(). This can avoid unnecessary intermediate allocations and lets map/filter/reduce-style pipelines process elements in one pass. Sorting is an important exception: it cannot be streamed element by element, so lazy sort, sort_ascending, and sort_descending first collect the current lazy pipeline's values, sort that collected vector, and then continue feeding the rest of the lazy chain.
#include"vector.h"// instead of <vector>const fcpp::vector<int> numbers({5, 1, 4, 2, 3});
constauto processed_numbers = numbers
// start a lazy pipeline from this point on
.lazy()
// this predicate is not evaluated yet
.filter([](constint& number) {
return number > 2;
})
// sorting is also deferred, but it needs to materialize the filtered// values internally when the terminal operation is called
.sort_ascending()
// this transform is not evaluated yet
.map<std::string>([](constint& number) {
returnstd::to_string(number);
})
// terminal operation: all stored operations are executed here
.get();
// processed_numbers -> fcpp::vector<std::string>({ "3", "4", "5" })// numbers -> fcpp::vector<int>({ 5, 1, 4, 2, 3 })Here is another example without sorting, thus all operations are materialized in the end.
constauto total = numbers
// start a lazy pipeline from this point on
.lazy()
// this transform is not evaluated yet
.map<int>([](constint& number) {
return number * 3;
})
// this predicate is not evaluated yet
.filter([](constint& number) {
return number > 5;
})
// terminal operation: all stored operations are executed here
.reduce(0, [](constint& partial_sum, constint& number) {
return partial_sum + number;
});
// total -> 42Lazy zip can combine a lazy vector with an fcpp::vector, a std::vector, or another fcpp::lazy_vector and also waits until a terminal operation is called, and only then checks that both sides have equal sizes. When zipping with another lazy vector, the right-hand lazy vector is materialized internally at that point, so its values can be paired by index.
const fcpp::vector<int> ages({32, 45, 37});
const fcpp::vector<std::string> names({"Jake", "Anna", "Kate"});
constauto employees = ages
// start a lazy pipeline from this point on
.lazy()
// zip is not evaluated yet
.zip(names)
// this transform is not evaluated yet
.map<person>([](const std::pair<int, std::string>& pair) {
returnperson(pair.first, pair.second);
})
// terminal operation: zip size validation and all stored operations run here
.get();
// employees -> fcpp::vector<person>({// person(32, "Jake"),// person(45, "Anna"),// person(37, "Kate"),// })#include"vector.h"// instead of <vector>const fcpp::vector numbers({1, 4, 2, 5, 8, 3, 1, 7, 1});
constauto first_index_of_one = numbers.find_first_index(1);
// returns 0
first_index_of_one.value();
constauto last_index_of_one = numbers.find_last_index(1);
// returns 8
last_index_of_one.value();
// all_indices_of_one -> { 0, 6, 8 }constauto all_indices_of_one = numbers.find_all_indices(1);
constauto index_of_nine = numbers.find_first_index(9);
// returns false
index_of_nine.has_value();#include"vector.h"// instead of <vector>
#include"index_range.h"
fcpp::vector<int> numbers({1, 4, 2, 5, 8, 3, 1, 7, 1});
// numbers -> fcpp::vector<int>({1, 4, 2, 5, 3, 1, 7, 1});
numbers.remove_at(4);
// numbers -> fcpp::vector<int>({4, 2, 5, 3, 1, 7, 1});
numbers.remove_front();
// numbers -> fcpp::vector<int>({4, 2, 5, 3, 1, 7});
numbers.remove_back();
// numbers -> fcpp::vector<int>({4, 2, 7});
numbers.remove_range(index_range::start_count(2, 3));
// numbers -> fcpp::vector<int>({4, 8, 2, 7});
numbers.insert_at(1, 8);
// numbers -> fcpp::vector<int>({-10, 4, 8, 2, 7});
numbers.insert_front(-10);
// numbers -> fcpp::vector<int>({-10, 4, 8, 2, 7, 9});
numbers.insert_back(9);
// numbers -> fcpp::vector<int>({-10, 4, 8, 3, -2, 5, 2, 7, 9});
numbers.insert_at(3, std::vector({3, -2, 5}));
// numbers -> fcpp::vector<int>({4, -6, 7, -10, 4, 8, 3, -2, 5, 2, 7, 9});
numbers.insert_front(fcpp::vector({4, -6, 7}));
// numbers -> fcpp::vector<int>({4, -6, 7, -10, 4, 8, 3, -2, 5, 2, 7, 9, 7, 3});
numbers.insert_back(std::initializer_list({7, 3}));#include"vector.h"// instead of <vector>// numbers.capacity() = 9// numbers.size() = 9
fcpp::vector<int> numbers({1, 4, 2, 5, 8, 3, 1, 7, 1});
// numbers -> fcpp::vector<int>({1, 4, 2, 5, 8});// numbers.capacity() = 9// numbers.size() = 5
numbers.resize(5);
// numbers -> fcpp::vector<int>({1, 4, 2, 5, 8, 0, 0});// numbers.capacity() = 9// numbers.size() = 7
numbers.resize(7);
// empty_numbers.capacity() = 0// empty_numbers.size() = 0
fcpp::vector<int> empty_numbers;
// empty_numbers.capacity() = 5// empty_numbers.size() = 0
empty_numbers.reserve(5);#include"vector.h"// instead of <vector>
fcpp::vector<int> numbers({1, 4, 2, 5, 8, 3, 1, 7, 1});
// returns true
numbers.all_of([](constint& number) {
return number < 10;
});
// returns false
numbers.all_of([](constint& number) {
return number > 2;
});
// returns true
numbers.any_of([](constint& number) {
return number < 5;
});
// returns false
numbers.any_of([](constint& number) {
return number > 9;
});
// returns true
numbers.none_of([](constint& number) {
return number < -2;
});
// returns false
numbers.none_of([](constint& number) {
return number > 7;
});Since C++17 several STL algorithms can be executed in parallel.
clang on macOS does not yet fully support the parallel execution model, however on Windows and Linux, an fcpp::vector supports the following parallel algorithms
for_each_parallel
map_parallel
filter_parallel
sort_parallel
sort_ascending_parallel
sort_descending_parallel
all_of_parallel
any_of_parallel
none_of_parallel#include"set.h"// instead of <set>// struct person as defined previouslystructperson_comparator {
booloperator() (const person& a, const person& b) const {
return a < b;
}
};
// ...// a set containing all colleaguesconst fcpp::set<person, person_comparator> colleagues({
person(51, "George"),
person(15, "Jake"),
person(18, "Jannet"),
person(41, "Jackie"),
person(25, "Kate")
});
// a set containing all friendsconst fcpp::set<person, person_comparator> friends({
person(51, "George"),
person(41, "Jackie"),
person(42, "Crystal"),
});
// find which colleagues are not friends// contains person(15, "Jake"), person(18, "Jannet") and person(25, "Kate")constauto colleagues_but_not_friends = colleagues.difference_with(friends);
// find which friends are colleagues// same as colleagues.intersect_with(friends)// contains person(51, "George"), person(41, "Jackie")constauto good_colleagues = friends.intersect_with(colleagues);
// a set of close family membersconst fcpp::set<person, person_comparator> family({
person(51, "Paul"),
person(81, "Barbara"),
});
// all of our friends and family for the next party invitation// contains person(51, "George"), person(41, "Jackie"), person(42, "Crystal"), person(51, "Paul"), person(81, "Barbara") constauto friends_and_family = friends.union_with(family);
// all set keys in a vectorconstauto people = friends_and_family.keys();#include"set.h"// instead of <set>// the employees' agesconst fcpp::set<int> ages({ 25, 45, 30, 63 });
// the employees' namesconst fcpp::set<std::string> names({ "Jake", "Bob", "Michael", "Philipp" });
constauto employees_below_40 = ages
// zip two sets for simultaneous processing
.zip(names)
// apply the functional map algorithm (transform from one type to another)
.map<person>([](const std::pair<int, std::string>& pair) { returnperson(pair.first, pair.second);
})
// filter the elements using a local function (lambda)
.filter([](const person& p) {
return p.age < 40;
});
/* prints the following: Jake is 30 years old. Bob is 25 years old.*/
employees_below_40.for_each([](const person& p) {
std::cout << p.name << " is " << p.age << " years old." << std::endl;
});
// total_age = 55constauto total_age = employees_below_40.reduce(0, [](constint& partial_sum, const person& p){
return partial_sum + p.age;
});Lazy sets are useful when chaining operations over a large set and only needing the final materialized set or a reduced value. A regular map().filter().reduce() chain creates intermediate sets and iterates once per algorithm. Calling .lazy() stores the following operations and executes them only when a terminal operation is called, such as get() or reduce(). This can avoid unnecessary intermediate allocations and lets map/filter/reduce-style pipelines process keys in one pass. Unlike vectors, sets are already ordered by their comparator, so lazy sets focus on the operations that make sense for set data: map, filter, difference_with, union_with, intersect_with, zip, and reduce.
#include"set.h"// instead of <set>const fcpp::set<int> numbers({1, 2, 3, 4, 5});
constauto total = numbers
// start a lazy pipeline from this point on
.lazy()
// this transform is not evaluated yet
.map<int>([](constint& number) {
return number * 3;
})
// this predicate is not evaluated yet
.filter([](constint& number) {
return number > 5;
})
// terminal operation: all stored operations are executed here
.reduce(0, [](constint& partial_sum, constint& number) {
return partial_sum + number;
});
// total -> 42Lazy set algebra can combine a lazy set with an fcpp::set, a std::set, or another fcpp::lazy_set. The operation is still deferred, but set algebra needs set membership and sorted set semantics, so the current lazy pipeline is materialized internally when the terminal operation is called. When the right-hand side is also lazy, it is materialized internally at the same point.
const fcpp::set<int> colleague_ages({15, 18, 25, 41, 51});
const fcpp::set<int> friend_ages({41, 42, 51});
const fcpp::set<int> family_ages({51, 81});
constauto guests = colleague_ages
// start a lazy pipeline from this point on
.lazy()
// this predicate is not evaluated yet
.filter([](constint& age) {
return age >= 18;
})
// set difference is not evaluated yet
.difference_with(friend_ages)
// set union is not evaluated yet
.union_with(family_ages)
// terminal operation: the lazy filter and set algebra run here
.get();
// guests -> fcpp::set<int>({18, 25, 51, 81})Lazy set zip can combine a lazy set with an fcpp::set, a std::set, an fcpp::vector, a std::vector, an fcpp::lazy_vector, or another fcpp::lazy_set. Size validation is deferred until a terminal operation is called. When zipping with a vector, duplicate vector values are removed before zipping, just like the eager set zip operation. When zipping with a lazy vector, the right-hand lazy vector is materialized internally at that point and then deduplicated. When zipping with another lazy set, the right-hand lazy set is materialized internally at that point, so its keys can be paired in set order.
const fcpp::set<int> ages({25, 45, 30, 63});
const fcpp::set<std::string> names({"Jake", "Bob", "Michael", "Philipp"});
constauto employees = ages
// start a lazy pipeline from this point on
.lazy()
// zip is not evaluated yet
.zip(names)
// this transform is not evaluated yet
.map<person>([](const std::pair<int, std::string>& pair) {
returnperson(pair.first, pair.second);
})
// terminal operation: zip size validation and all stored operations run here
.get();
// employees -> fcpp::set<person>({// person(25, "Bob"),// person(30, "Jake"),// person(45, "Michael"),// person(63, "Philipp"),// })#include"set.h"// instead of <set>
fcpp::set<int> numbers({1, 4, 2, 5, 8, 3, 7});
// returns true
numbers.all_of([](constint& number) {
return number < 10;
});
// returns false
numbers.all_of([](constint& number) {
return number > 2;
});
// returns true
numbers.any_of([](constint& number) {
return number < 5;
});
// returns false
numbers.any_of([](constint& number) {
return number > 9;
});
// returns true
numbers.none_of([](constint& number) {
return number < -2;
});
// returns false
numbers.none_of([](constint& number) {
return number > 7;
});#include"set.h"// instead of <set>
fcpp::set<int> numbers({1, 2, 3, 4, 5, 7, 8});
// numbers -> fcpp::set<int>({1, 2, 3, 5, 7, 8});
numbers.remove(4);
// numbers -> fcpp::set<int>({1, 2, 3, 5, 7, 8, 10});
numbers.insert(10);
// returns true
numbers.contains(10);
// returns false
numbers.contains(25);
// returns 7
numbers.size();
// removes all keys
numbers.clear();#include"map.h"// instead of <map>const fcpp::map<std::string, int> ages({
{"jake", 32},
{"mary", 16},
{"david", 40}
});
// keep only persons above 18 years oldconstauto adults = ages
.filtered([](const std::pair<const std::string, int>& element) {
return element.second >= 18;
});
// ages_by_initial -> fcpp::map<char, std::string>({{'d', "40 years"}, {'j', "32 years"}})constauto ages_by_initial = adults.map_to<char, std::string>([](const std::pair<const std::string, int>& element) {
returnstd::make_pair(element.first[0], std::to_string(element.second) + " years");
});
// total_age = 72constauto total_age = adults.reduce(0, [](constint& partial_sum, const std::pair<const std::string, int>& element) {
return partial_sum + element.second;
});
/* prints the following: jake is 32 years old. david is 40 years old.*/
adults.for_each([](const std::pair<const std::string, int>& element) {
std::cout << element.first << " is " << element.second << " years old." << std::endl;
});Lazy maps are useful when chaining map_to, filter, and reduce over a large map. A regular filtered().map_to().reduce() style chain creates intermediate maps and iterates once per algorithm. Calling .lazy() stores the following operations and executes them only when a terminal operation is called, such as get() or reduce(). This can avoid unnecessary intermediate allocations and lets map_to/filter/reduce-style pipelines process key/value pairs in one pass. When a lazy map_to creates equivalent output keys, the first key/value pair encountered in sorted map order is kept, following std::map::insert semantics.
#include"map.h"// instead of <map>const fcpp::map<std::string, int> ages({
{"jake", 32},
{"mary", 16},
{"david", 40}
});
constauto ages_by_initial = ages
// start a lazy pipeline from this point on
.lazy()
// this predicate is not evaluated yet
.filter([](const std::pair<const std::string, int>& element) {
return element.second >= 18;
})
// this transform is not evaluated yet
.map_to<char, std::string>([](const std::pair<const std::string, int>& element) {
returnstd::make_pair(element.first[0], std::to_string(element.second) + " years");
})
// terminal operation: all stored operations are executed here
.get();
// ages_by_initial -> fcpp::map<char, std::string>({{'d', "40 years"}, {'j', "32 years"}})// ages -> fcpp::map<std::string, int>({{"david", 40}, {"jake", 32}, {"mary", 16}})constauto total_age = ages
// start a lazy pipeline from this point on
.lazy()
// this predicate is not evaluated yet
.filter([](const std::pair<const std::string, int>& element) {
return element.second >= 18;
})
// terminal operation: all stored operations are executed here
.reduce(0, [](constint& partial_sum, const std::pair<const std::string, int>& element) {
return partial_sum + element.second;
});
// total_age -> 72#include"map.h"// instead of <map>const fcpp::map<std::string, int> ages({
{"jake", 32},
{"mary", 26},
{"david", 40}
});
// returns true
ages.all_of([](const std::pair<const std::string, int>& element) {
return element.second > 20;
});
// returns false
ages.all_of([](const std::pair<const std::string, int>& element) {
return element.second < 35;
});
// returns true
ages.any_of([](const std::pair<const std::string, int>& element) {
return element.second == 40;
});
// returns false
ages.any_of([](const std::pair<const std::string, int>& element) {
return element.second > 50;
});
// returns true
ages.none_of([](const std::pair<const std::string, int>& element) {
return element.second < 18;
});
// returns false
ages.none_of([](const std::pair<const std::string, int>& element) {
return element.second == 26;
});#include"map.h"// instead of <map>
fcpp::map<std::string, int> ages({
{"jake", 32},
{"mary", 26},
{"david", 40}
});
// names -> fcpp::vector<std::string>({"david", "jake", "mary"})constauto names = ages.keys();
// years -> fcpp::vector<int>({40, 32, 26})constauto years = ages.values();
// ages -> fcpp::map<std::string, int>({{"david", 40}, {"jake", 32}})
ages.remove("mary");
// ages -> fcpp::map<std::string, int>({{"anna", 28}, {"david", 40}, {"jake", 32}}), mary has already been removed
ages.insert("anna", 28);
// without_jake -> fcpp::map<std::string, int>({{"anna", 28}, {"david", 40}})constauto without_jake = ages.removing("jake");
// with_paul -> fcpp::map<std::string, int>({{"anna", 28}, {"david", 40}, {"jake", 32}, {"paul", 51}})constauto with_paul = ages.inserting("paul", 51);