diff --git a/README.md b/README.md index 797dfe0..fa5090a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # fpgen *Functional programming in C++ using C++20 coroutines* -![](https://img.shields.io/badge/test_coverage-98%25-brightgreen) +![](https://img.shields.io/badge/test_coverage-97%25-brightgreen) ## Aim @@ -17,14 +17,9 @@ Currently supported features: - Lazy `map`ping over generators. - Lazy `zip`ping of generators. - Lazy `filter`ing of generators. - -Planned/In progress features: - Commonly used aggregators: - Lazy `fold`ing of generators. - Lazy `sum`ming of generators. - - Ergonomic improvements: - - Enable streaming `<<` of generators. - - Simple structure which allows generator construction, manipulation and aggregation using member functions. Got another idea? Drop a feature request on the repo. diff --git a/inc/aggregators.hpp b/inc/aggregators.hpp index 898d2c1..8297dd9 100644 --- a/inc/aggregators.hpp +++ b/inc/aggregators.hpp @@ -28,7 +28,7 @@ namespace fpgen { */ template typename Container> -Container &aggregate_to(generator &gen, +Container &aggregate_to(generator gen, Container &out) { while (gen) { out.push_back(gen()); @@ -55,7 +55,7 @@ Container &aggregate_to(generator &gen, template typename Container> Container & -tup_aggregate_to(generator> &gen, +tup_aggregate_to(generator> gen, Container &out) { while (gen) { std::tuple tup = gen(); @@ -75,7 +75,7 @@ tup_aggregate_to(generator> &gen, * \param[in,out] gen The generator to iterate over. * \returns The amount of elements in the generator. */ -template size_t count(generator &gen) { +template size_t count(generator gen) { size_t cnt = 0; while (gen) { gen(); @@ -102,7 +102,7 @@ template size_t count(generator &gen) { * \returns The final accumulator value. */ template -TOut fold(generator &gen, Fun folder) { +TOut fold(generator gen, Fun folder) { TOut value = {}; while (gen) { value = folder(value, gen()); @@ -129,7 +129,7 @@ TOut fold(generator &gen, Fun folder) { * \returns The final accumulator value. */ template -TOut fold(generator &gen, Fun folder, TOut initial) { +TOut fold(generator gen, Fun folder, TOut initial) { TOut value(initial); while (gen) { value = folder(value, gen()); @@ -157,7 +157,7 @@ TOut fold(generator &gen, Fun folder, TOut initial) { * now the output value. */ template -TOut &fold_ref(generator &gen, Fun folder, TOut &initial) { +TOut &fold_ref(generator gen, Fun folder, TOut &initial) { while (gen) { initial = folder(initial, gen()); } @@ -175,7 +175,7 @@ TOut &fold_ref(generator &gen, Fun folder, TOut &initial) { * \param[in,out] gen The generator to sum over. * \returns The sum of all elements. */ -template T sum(generator &gen) { +template T sum(generator gen) { T accum = {}; while (gen) { accum = accum + gen(); @@ -195,7 +195,7 @@ template T sum(generator &gen) { * \param[in,out] gen The generator to iterate over. * \param[in] func The function to use. */ -template void foreach (generator &gen, Fun func) { +template void foreach (generator gen, Fun func) { while (gen) { func(gen()); } diff --git a/inc/fpgen.hpp b/inc/fpgen.hpp index 2aa2d20..6afedfe 100644 --- a/inc/fpgen.hpp +++ b/inc/fpgen.hpp @@ -1,8 +1,10 @@ #ifndef _FPGEN_MAIN #define _FPGEN_MAIN +#include "aggregators.hpp" #include "generator.hpp" #include "manipulators.hpp" #include "sources.hpp" +#include "stream.hpp" #endif diff --git a/inc/generator.hpp b/inc/generator.hpp index 17e8ae5..c2f8efe 100644 --- a/inc/generator.hpp +++ b/inc/generator.hpp @@ -100,7 +100,10 @@ template = true> class generator { * coroutine calls `co_return`, the environment will call this function. * \param[in] v The value to set. */ - void return_value(value_type v) { value = v; } + // void return_value(value_type v) { value = v; } + + void return_void() {} + /** * \brief Sets an intermediate value from the coroutine. * @@ -155,12 +158,12 @@ template = true> class generator { * This value should only be true when the underlying generator is * finished. Iterators are compared based on this value. */ - bool is_finished; + // bool is_finished; /** * \brief The last value from the coroutine (and thus, the value currently * held by the generator's promise). */ - value_t value; + // value_t value; /** * \brief Constructs a new iterator from a source generator. @@ -173,8 +176,7 @@ template = true> class generator { * modified). * \param[in] is_finised Is this an `end` iterator? */ - iterator_type(gen_t &source, bool is_finised) - : source{source}, is_finished{is_finised} { + iterator_type(gen_t &source, bool is_finised) : source{source} { if (!is_finised) ++(*this); } @@ -190,20 +192,15 @@ template = true> class generator { * \param[in] is_finised Is this iterator finished? * \param[in] value The value this iterator should hold. */ - iterator_type(gen_t &source, bool is_finished, value_t value) - : source{source}, is_finished{is_finished}, value{value} {} + iterator_type(gen_t &source) : source{source} {} /** * \brief Steps the generator to the next value. * \returns A new iterator for this generator, with the same state. */ iter_t operator++() { - if (!source) - is_finished = true; - value = source(); - if (!source) - is_finished = true; - return {source, is_finished, value}; + source.next(); + return {source}; } /** @@ -219,19 +216,20 @@ template = true> class generator { * \param[in] other The iterator to compare against. * \returns False if both are equal, otherwise true. */ - bool operator!=(const iter_t &other) { - return is_finished != other.is_finished; - } + bool operator!=(const iter_t &) { return static_cast(source); } /** * \brief Gets the current value from the iterator. * \returns The current value. */ - value_t operator*() { return value; } + value_t operator*() { + source.contains = false; + return source._h.promise().value; + } /** * \brief Converts this iterator to the current value. * \returns The current value. */ - operator value_t() { return value; } + operator value_t() { return source._h.promise().value; } }; /** @@ -250,12 +248,13 @@ template = true> class generator { * This method should only be called from the environment. * \param[in] p The promise to use. */ - generator(promise_type &p) : _h{handle_type::from_promise(p)} {} + generator(promise_type &p) + : _h{handle_type::from_promise(p)}, contains{false} {} /** * \brief Copy-constructing generators results in undefined behaviour. */ - generator(const generator &other) = delete; + generator(const generator &other) = default; /** * \brief Moves the data from the other generator into this one. * \param[in,out] other The other generator. @@ -265,7 +264,7 @@ template = true> class generator { /** * \brief Copy-assigning generators results in undefined behaviour. */ - generator &operator=(const generator &other) = delete; + generator &operator=(const generator &other) = default; /** * \brief Moves the data from the other generator into this one. * \param[in,out] other The other generator. @@ -287,7 +286,12 @@ template = true> class generator { * \brief Converts this generator to a bool. * \returns True if more values remain, otherwise false. */ - operator bool() const { return !_h.done(); } + operator bool() { + if (_h.done()) + return false; + next(); + return !_h.done(); + } /** * \brief Gets an iterator to the current coroutine state. @@ -312,15 +316,23 @@ template = true> class generator { * that can be thrown from the coroutine. */ value_type operator()() { - if (*this) - _h(); - if (_h.promise().ex) - std::rethrow_exception(_h.promise().ex); - return _h.promise().value; + next(); + contains = false; + return std::move(_h.promise().value); } private: handle_type _h; + bool contains; + + void next() { + if (!contains) { + _h(); + if (_h.promise().ex) + std::rethrow_exception(_h.promise().ex); + contains = true; + } + } }; } // namespace fpgen diff --git a/inc/manipulators.hpp b/inc/manipulators.hpp index 7f095c0..0a6ac60 100644 --- a/inc/manipulators.hpp +++ b/inc/manipulators.hpp @@ -25,11 +25,12 @@ namespace fpgen { * mapping function. */ template -auto map(generator &gen, Fun func) +auto map(generator gen, Fun func) -> generator::type> { while (gen) { co_yield func(gen()); } + co_return; } /** @@ -46,10 +47,11 @@ auto map(generator &gen, Fun func) * \returns A new generator containing tuples of values from both generators. */ template -generator> zip(generator &gen1, generator &gen2) { +generator> zip(generator gen1, generator gen2) { while (gen1 && gen2) { co_yield {gen1(), gen2()}; } + co_return; } /** @@ -62,18 +64,124 @@ generator> zip(generator &gen1, generator &gen2) { * \tparam T The type contained in the generator. * \tparam Pred The function type of the predicate function. Should return a * bool. - * \param gen The generator containing the original values. - * \param p The predicate. + * \param[in,out] gen The generator containing the original values. + * \param[in] p The predicate. * \returns A new generator which yields all values in the original generator * except those not matching the predicate. */ template -generator filter(generator &gen, Pred p) { +generator filter(generator gen, Pred p) { while (gen) { T val(gen()); if (p(val)) co_yield val; } + co_return; +} + +/** + * \brief Ignores the first n elements from a generator. + * + * Extracts the first `count` elements from the generator and throws those + * away. All subsequent elements in the generator are passed through as normal. + * If there aren't enough elements in the original generator, an empty generator + * is returned. + * + * \tparam T The type contained in the generator. + * \param[in,out] gen The generator to drop from. + * \param[in] count The amount of elements to ignore. + * \returns A new generator yielding all values except the first n. + */ +template generator drop(generator gen, size_t count) { + for (size_t i = 0; i < count && gen; i++) { + gen(); + } + + while (gen) { + co_yield gen(); + } + co_return; +} + +/** + * \brief Yields the first n elements from a generator. + * + * Extracts and yields exactly `count` elements (or less if the generator + * doesn't contain that much values). The iteration is then stopped and no + * further elements will be generated. If generation has side effects, the side + * effects of elements after the first n will not be observable. + * + * \tparam T The type contained in the generator. + * \param[in,out] gen The generator to take elements from. + * \param[in] count The amount of elements to yield. + * \returns A new generator yielding only the first n values. + */ +template generator take(generator gen, size_t count) { + for (size_t i = 0; i < count && gen; i++) { + co_yield gen(); + } + co_return; +} + +/** + * \brief Drops elements while they satisfy a certain predicate. + * + * Iterates over the elements in the generator and drops them until it + * encounters a value not satisfying the predicate (`!p(value)` is true). Then + * it starts yielding each remaining value in the generator. To remove all + * values satisfying `p`, use `fpgen::filter(gen, [&p](auto v) { return !p(v); + * });`. + * + * \tparam T The type contained in the generator. + * \tparam Pred The type of the predicate (should be a T -> bool function). + * \param gen The generator to drop elements from. + * \param p The predicate. + * \returns A new generator where the first element is guaranteed to not + * satisfy `p`. + */ +template +generator drop_while(generator gen, Pred p) { + while (gen) { + T temp = gen(); + if (!p(temp)) { + co_yield temp; + break; + } + } + + while (gen) { + co_yield gen(); + } + co_return; +} + +/** + * \brief Yields elements while they satisfy a certain predicate. + * + * Iterates over the elements in the generator and yields them until it + * encounters a value not satisfying the predicate (`!p(value)`). Then it stops + * generating (any side effects from the subsequent elements won't be + * observable). To obtain a generator with all elements satisfying `p`, use + * `fpgen::filter(gen, p);` instead. + * + * \tparam T The type contained in the generator. + * \tparam Pred The type of the predicate (should be a T -> bool function). + * \param gen The generator to take elements from. + * \param p The predicate. + * \returns A new generator where all elements are guaranteed to satisfy the + * predicate and were generated before any element which didn't satisfy the + * predicate. + */ +template +generator take_while(generator gen, Pred p) { + while (gen) { + T val = gen(); + if (!p(val)) { + break; + } + co_yield val; + } + co_return; } } // namespace fpgen diff --git a/inc/sources.hpp b/inc/sources.hpp index ac7eeb0..e3fb577 100644 --- a/inc/sources.hpp +++ b/inc/sources.hpp @@ -2,7 +2,6 @@ #define _FPGEN_SOURCES #include "generator.hpp" -#include #include /** @@ -29,6 +28,7 @@ generator from(const Container &cont) { for (auto it = std::begin(cont); it != std::end(cont); ++it) { co_yield *it; } + co_return; } /** @@ -54,6 +54,7 @@ generator> enumerate(const Container &cont) { co_yield {i, *it}; i++; } + co_return; } /** @@ -79,6 +80,7 @@ from_tup(const Container &cont) { for (auto it = cont.begin(); it != cont.end(); ++it) { co_yield *it; } + co_return; } /** diff --git a/test/Makefile b/test/Makefile index 1e76973..731ae90 100644 --- a/test/Makefile +++ b/test/Makefile @@ -1,6 +1,6 @@ SOURCES=$(shell find $(SRCD) -name '*.cpp') DEPS=$(SOURCES:$(SRCD)/%.cpp=$(OBJD)/%.d) -TESTS=generator sources manip aggreg +TESTS=generator sources manip aggreg stream chain TESTOBJ=$(TESTS:%=$(OBJD)/test_%.o) CONAN_PKG_OVERRIDE=gtest diff --git a/test/src/test_aggreg.cpp b/test/src/test_aggreg.cpp index 7ea4304..259163e 100644 --- a/test/src/test_aggreg.cpp +++ b/test/src/test_aggreg.cpp @@ -1,13 +1,14 @@ #include "aggregators.hpp" #include "generator.hpp" #include "manipulators.hpp" +#include "sources.hpp" #include "gmock/gmock.h" #include "gtest/gtest.h" #include #include -fpgen::generator a_empty() { co_return 0; } +fpgen::generator a_empty() { co_return; } fpgen::generator values() { size_t i = 0; @@ -15,14 +16,14 @@ fpgen::generator values() { co_yield 0; co_yield 1; - while (j < 21) { + while (j <= 21) { size_t tmp = i + j; co_yield tmp; i = j; j = tmp; } - co_return i + j; + // co_return i + j; } size_t calc_sum() { @@ -46,7 +47,7 @@ size_t sum_ref(size_t &old, size_t in) { TEST(aggregate, empty) { auto gen = a_empty(); - gen(); + // gen(); std::vector res; EXPECT_EQ(0, fpgen::aggregate_to(gen, res).size()); } @@ -69,6 +70,14 @@ TEST(aggregate, vector) { EXPECT_EQ(res.size(), 10); } +TEST(aggregate, vec_to_vec) { + std::vector in = {0, 1, 2, 3, 4, 5, 6}; + std::vector out = {}; + auto gen = fpgen::from(in); + out = fpgen::aggregate_to(gen, out); + EXPECT_EQ(in, out); +} + TEST(aggregate, map) { fpgen::generator sources[2] = {values(), values()}; auto gen = fpgen::zip(sources[0], sources[1]); @@ -89,7 +98,7 @@ TEST(aggregate, map) { TEST(aggregate, count_empty) { auto gen = a_empty(); - gen(); + // gen(); EXPECT_EQ(0, fpgen::count(gen)); } @@ -100,7 +109,7 @@ TEST(aggregate, count) { TEST(fold, fold_noin_empty) { auto gen = a_empty(); - gen(); + // gen(); EXPECT_EQ(0, fpgen::fold(gen, sum)); } @@ -111,7 +120,7 @@ TEST(fold, fold_noin) { TEST(fold, fold_in_noref_empty) { auto gen = a_empty(); - gen(); + // gen(); EXPECT_EQ(7, fpgen::fold(gen, sum, 7)); } @@ -122,7 +131,7 @@ TEST(fold, fold_in_noref) { TEST(fold, fold_in_ref_empty) { auto gen = a_empty(); - gen(); + // gen(); size_t res = 7; EXPECT_EQ(7, fpgen::fold_ref(gen, sum, res)); EXPECT_EQ(7, res); @@ -137,7 +146,7 @@ TEST(fold, fold_in_ref) { TEST(sum, empty) { auto gen = a_empty(); - gen(); + // gen(); EXPECT_EQ(0, fpgen::sum(gen)); } @@ -148,7 +157,7 @@ TEST(sum, normal) { TEST(foreach, empty) { auto gen = a_empty(); - gen(); + // gen(); size_t res = 0; fpgen::foreach (gen, [&res](size_t val) { res += val; }); EXPECT_EQ(res, 0); diff --git a/test/src/test_chain.cpp b/test/src/test_chain.cpp new file mode 100644 index 0000000..f9a4158 --- /dev/null +++ b/test/src/test_chain.cpp @@ -0,0 +1,36 @@ +#include "aggregators.hpp" +#include "generator.hpp" +#include "manipulators.hpp" +#include "sources.hpp" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using namespace fpgen; + +template +std::ostream &operator<<(std::ostream &in, std::tuple &other) { + return in << "{ " << std::get<0>(other) << ", " << std::get<1>(other) << " }"; +} + +TEST(chain, simple_chain) { + /* + Chain: + -> [0..] -> [5..] -> [5..13] -> [15...169] \ + -> {[15...169], ['h'...'p']} + -> [0..] -> [7..] -> [7..22] -> ['h'...'w'] / + */ + auto gen = + map(take(drop(inc((size_t)0), 5), 9), [](size_t in) { return in * in; }); + + auto second = map(take(drop(inc((size_t)0), 7), 22), + [](size_t in) { return (char)('a' + (in % 26)); }); + + size_t value = 5; + for (auto v : zip(gen, second)) { + EXPECT_EQ(value * value, std::get<0>(v)); + EXPECT_EQ('a' + 2 + value, std::get<1>(v)); + EXPECT_TRUE(value <= 13); + value++; + } + EXPECT_EQ(value, 14); +} diff --git a/test/src/test_generator.cpp b/test/src/test_generator.cpp index 231f29a..a63e552 100644 --- a/test/src/test_generator.cpp +++ b/test/src/test_generator.cpp @@ -3,7 +3,7 @@ #include "gtest/gtest.h" #include -fpgen::generator empty() { co_return 0; } +fpgen::generator empty() { co_return; } fpgen::generator infinite() { int value = 0; @@ -14,10 +14,10 @@ fpgen::generator infinite() { } fpgen::generator finite_squares(int min, int max) { - for (int val = min; val < max; val++) { + for (int val = min; val <= max; val++) { co_yield val *val; } - co_return max *max; + co_return; // max *max; } TEST(generator, accept_empty_gen) { @@ -27,7 +27,7 @@ TEST(generator, accept_empty_gen) { TEST(generator, iterator_empty_gen) { auto gen = empty(); - gen(); + // gen(); for (auto v : gen) { FAIL(); } diff --git a/test/src/test_manip.cpp b/test/src/test_manip.cpp index aeb1521..9df732a 100644 --- a/test/src/test_manip.cpp +++ b/test/src/test_manip.cpp @@ -9,22 +9,22 @@ #include #include -fpgen::generator manip_empty() { co_return 0; } +fpgen::generator manip_empty() { co_return; } fpgen::generator manip() { size_t i = 1; - while (i < 1024) { + while (i <= 1024) { co_yield i; i *= 2; } - co_return i; + co_return; // i; } fpgen::generator until12() { - for (int i = 0; i < 12; i++) { + for (int i = 0; i <= 12; i++) { co_yield i; } - co_return 12; + co_return; // 12; } size_t mapper(size_t v) { return v * v; } @@ -33,7 +33,7 @@ bool over_100(size_t v) { return (v > 100); } TEST(manipulators, map_empty) { auto gen = manip_empty(); - gen(); + // gen(); for (auto v : fpgen::map(gen, mapper)) { FAIL() << "Should not return a value"; @@ -54,8 +54,8 @@ TEST(manipulators, map) { TEST(manipulators, zip_both_empty) { auto gen = manip_empty(); auto gen2 = manip_empty(); - gen(); - gen2(); + // gen(); + // gen2(); for (auto v : fpgen::zip(gen, gen2)) { FAIL() << "Should not return a value"; @@ -66,7 +66,7 @@ TEST(manipulators, zip_both_empty) { TEST(manipulators, zip_first_empty) { auto gen = manip_empty(); auto gen2 = fpgen::inc((size_t)0); - gen(); + // gen(); for (auto v : fpgen::zip(gen, gen2)) { FAIL() << "Should not return a value"; @@ -77,7 +77,7 @@ TEST(manipulators, zip_first_empty) { TEST(manipulators, zip_second_empty) { auto gen = fpgen::inc((size_t)0); auto gen2 = manip_empty(); - gen2(); + // gen2(); for (auto v : fpgen::zip(gen, gen2)) { FAIL() << "Should not return a value"; @@ -105,7 +105,7 @@ TEST(manipulators, zip_none_empty) { TEST(manipulators, filter_empty) { auto gen = manip_empty(); - gen(); + // gen(); size_t i = 0; @@ -133,3 +133,92 @@ TEST(manipulators, filter_normal) { i += 2; } } + +TEST(manipulators, drop_empty) { + auto gen = drop(manip_empty(), 5); + for (auto v : gen) { + FAIL() << "should not return a value"; + } + SUCCEED(); +} + +TEST(manipulators, drop_normal) { + auto gen = drop(until12(), 5); + size_t exp = 5; + for (auto v : gen) { + EXPECT_EQ(v, exp); + EXPECT_TRUE(exp <= 12); + exp++; + } + EXPECT_EQ(exp, 13); +} + +TEST(manipulators, take_empty) { + auto gen = take(manip_empty(), 4); + for (auto v : gen) { + FAIL() << "should not return a value"; + } + SUCCEED(); +} + +TEST(manipulators, take_normal) { + auto gen = take(fpgen::inc((size_t)0), 8); + size_t exp = 0; + for (auto v : gen) { + EXPECT_EQ(v, exp); + EXPECT_TRUE(exp <= 8); + exp++; + } + EXPECT_EQ(exp, 8); +} + +TEST(manipulators, drop_take) { + auto gen = take(drop(fpgen::inc((size_t)0), 4), 9); + for (size_t exp = 4; exp < 13; exp++) { + EXPECT_TRUE(static_cast(gen)); + EXPECT_EQ(exp, gen()); + } + + for (auto v : gen) { + FAIL() << "should not return a value"; + } + SUCCEED(); +} + +TEST(manipulators, drop_while_empty) { + auto gen = drop_while(manip_empty(), [](size_t v) { return v > 3; }); + for (auto v : gen) { + FAIL() << "should not return a value"; + } + SUCCEED(); +} + +TEST(manipulators, drop_while_normal) { + auto gen = drop_while(until12(), [](size_t v) { return v < 5; }); + size_t exp = 5; + for (auto v : gen) { + EXPECT_EQ(v, exp); + EXPECT_TRUE(exp <= 12); + exp++; + } + EXPECT_EQ(exp, 13); +} + +TEST(manipulators, take_while_empty) { + auto gen = take_while(manip_empty(), [](size_t v) { return v < 4; }); + for (auto v : gen) { + FAIL() << "should not return a value"; + } + SUCCEED(); +} + +TEST(manipulators, take_while_normal) { + auto gen = take_while(fpgen::inc((size_t)0), [](size_t v) { return v < 8; }); + size_t exp = 0; + for (auto v : gen) { + EXPECT_EQ(v, exp); + EXPECT_TRUE(exp <= 8); + exp++; + } + EXPECT_EQ(exp, 8); +} diff --git a/test/src/test_stream.cpp b/test/src/test_stream.cpp new file mode 100644 index 0000000..d265778 --- /dev/null +++ b/test/src/test_stream.cpp @@ -0,0 +1,30 @@ +#include "aggregators.hpp" +#include "generator.hpp" +#include "manipulators.hpp" +#include "sources.hpp" +#include "stream.hpp" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +// temporary +#include + +char *demangle(const char *name) { + return abi::__cxa_demangle(name, nullptr, nullptr, nullptr); +} + +char mapper(int i) { return 'a' + i; } +fpgen::generator map(fpgen::generator in) { return map(in, mapper); } + +TEST(stream, inout) { + std::vector in = {0, 1, 2, 3}; + fpgen::stream strm = fpgen::from(in); + std::vector output; + strm >> output; + + std::vector expected; + fpgen::generator gen = fpgen::from(in); + expected = fpgen::aggregate_to(gen, expected); + + EXPECT_EQ(output, expected); +}