{"id":13729446,"url":"https://github.com/ast-al/rangeless","last_synced_at":"2025-05-08T01:32:34.058Z","repository":{"id":46886165,"uuid":"194420704","full_name":"ast-al/rangeless","owner":"ast-al","description":"c++ LINQ -like library of higher-order functions for data manipulation","archived":false,"fork":false,"pushed_at":"2021-03-14T12:54:34.000Z","size":1345,"stargazers_count":194,"open_issues_count":3,"forks_count":6,"subscribers_count":16,"default_branch":"master","last_synced_at":"2024-08-04T02:08:39.954Z","etag":null,"topics":["cpp","cpp11","functional","functional-programming","itertools","lazy-evaluation","linq","parallel","pipeline","range","streaming-algorithms","streaming-data"],"latest_commit_sha":null,"homepage":"","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ast-al.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-06-29T15:31:15.000Z","updated_at":"2024-07-29T11:41:34.000Z","dependencies_parsed_at":"2022-09-03T13:53:11.928Z","dependency_job_id":null,"html_url":"https://github.com/ast-al/rangeless","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ast-al%2Frangeless","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ast-al%2Frangeless/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ast-al%2Frangeless/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ast-al%2Frangeless/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ast-al","download_url":"https://codeload.github.com/ast-al/rangeless/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224688987,"owners_count":17353301,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cpp","cpp11","functional","functional-programming","itertools","lazy-evaluation","linq","parallel","pipeline","range","streaming-algorithms","streaming-data"],"created_at":"2024-08-03T02:01:00.317Z","updated_at":"2024-11-14T20:30:48.736Z","avatar_url":"https://github.com/ast-al.png","language":"C++","readme":"# rangeless::fn\n## `range`-free LINQ-like library of higher-order functions for manipulation of containers and lazy input-sequences.\n\n\n### [Documentation](https://ast-al.github.io/rangeless/docs/html/namespacerangeless_1_1fn.html)\n\n\n### What it's for\n- Reduce the amount of mutable state.\n- Flatten control-flow.\n- Lessen the need to deal with iterators directly.\n- Make the code more expressive and composeable.\n\nThis library is intended for moderate to advanced-level c++ programmers that like the idea of c++ `ranges`, but can't or choose not to use them for various reasons (e.g. high complexity, compilation overhead, debug-build performance, size of the library, etc).\n\nMotivations:\n- https://www.fluentcpp.com/2019/09/13/the-surprising-limitations-of-c-ranges-beyond-trivial-use-cases/\n- https://www.fluentcpp.com/2019/02/12/the-terrible-problem-of-incrementing-a-smart-iterator/\n- https://brevzin.github.io/c++/2020/07/06/split-view/\n- http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p2011r1.html\n- https://aras-p.info/blog/2018/12/28/Modern-C-Lamentations/\n\n\n### Features\n- Portable c++11. (examples are c++14)\n- Single-header.\n- Minimal standard library dependencies.\n- No inheritance, polymorphism, type-erasures, ADL, advanced metaprogramming, enable_ifs, concepts, preprocessor magic, arcane programming techniques (for some definition of arcane), or compiler-specific workarounds.\n- Low `#include` and compile-time overhead.\n- Enables trivial parallelization (see [`fn::to_async and fn::transform_in_parallel`](https://ast-al.github.io/rangeless/docs/html/group__parallel.html)).\n- Allows for trivial extension of functionality (see [`fn::adapt`](https://ast-al.github.io/rangeless/docs/html/group__transform.html)).\n\n\n### Simple examples\n```cpp\nstruct employee_t\n{\n            int id;\n    std::string last_name;\n    std::string first_name;\n            int years_onboard;\n\n    bool operator\u003c(const employee_t\u0026 other) const\n    {\n        return id \u003c other.id;\n    }\n};\n\nnamespace fn = rangeless::fn;\nusing fn::operators::operator%;   // arg % fn   equivalent to fn(std::forward\u003cArg\u003e(arg))\nusing fn::operators::operator%=;  // arg %= fn; equivalent to arg = fn( std::move(arg));\n\n// Abbreviated lambda macro, see http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0573r2.html\n// (It is not defined in this library, because nobody likes macros except those we define ourselves).\n#define L(expr) ([\u0026](auto\u0026\u0026 _ ){ return expr; })\n\nauto employees = std::vector\u003cemployee_t\u003e{/*...*/};\n\nemployees %= fn::where L( _.last_name != \"Doe\" );\nemployees %= fn::take_top_n_by(10, L( _.years_onboard ));\nemployees %= fn::sort_by L( std::tie( _.last_name, _.first_name) );\n\n// or:\n\nemployees = std::move(employees)\n          % fn::where L( _.last_name != \"Doe\" )\n          % fn::take_top_n_by(10, L( _.years_onboard ))\n          % fn::sort_by L( std::tie( _.last_name, _.first_name) );\n```\n\nHow does this work? E.g. `fn::sort_by(projection_fn)` is a higher-order function that returns a unary function that takes inputs by value (normally passed as rvalue), sorts them by the user-provided projection, and returns them by value (non-copying). Similarly, `fn::where(predicate)` filters the input to those satisfying the predicate.\n\n`operator %`, pronounced \"then\", invoked as `arg % unary_function`, is syntax-sugar, similar to F#'s operator `|\u003e`, that enables structuring your code in top-down manner, consistent with the direction of the data-flow, similar to UNIX pipes. It is implemented as:\n```cpp\n    template\u003ctypename Arg, typename F\u003e\n    auto operator % (Arg\u0026\u0026 arg, F\u0026\u0026 fn) -\u003e decltype( std::forward\u003cF\u003e(fn)( std::forward\u003cArg\u003e(arg)) )\n    {\n        return std::forward\u003cF\u003e(fn)( std::forward\u003cArg\u003e(arg));\n    }\n```\n\nWithout using `operator%` the above example would look as follows:\n```cpp\nemployees = fn::where L( _.last_name != \"Doe\" )( std::move(employees) );\nemployees = fn::take_top_n_by(10, L( _.years_onboard ))( std::move(employees) );\nemployees = fn::sort_by L( std::tie( _.last_name, _.first_name) )( std::move(employees) );\n\n// or, as single nested function call:\n\nemployees = fn::sort_by L( std::tie( _.last_name, _.first_name))(\n                fn::take_top_n_by(10, L( _.years_onboard ))(\n                    fn::where L( _.last_name != \"Doe\" )(\n                        std::move(employees) )));\n```\n\n\n### Example: Top-5 most frequent words chosen among the words of the same length.\n```cpp\n    auto my_isalnum = L( std::isalnum(_) || _ == '_' );\n\n    fn::from( std::istreambuf_iterator\u003cchar\u003e(istr.rdbuf()), {})\n  % fn::transform L( char(std::tolower(_)) )\n  % fn::group_adjacent_by(my_isalnum)             // returns sequence-of-std::string\n  % fn::where L( my_isalnum( _.front()))          // discard strings with punctuation\n  % fn::counts()                                  // returns map\u003cstring,size_t\u003e of word-\u003ecount\n  % fn::group_all_by L( _.first.size())           // returns [[(word, count)]], each subvector containing words of same length\n  % fn::transform(                                // transform each sub-vector...\n        fn::take_top_n_by(5UL, L( _.second))      // by filtering it taking top-5 by count.\n  % fn::concat()                                  // undo group_all_by (flatten)\n  % fn::for_each L( void(std::cout \u003c\u003c _.first \u003c\u003c \"\\t\" \u003c\u003c _.second \u003c\u003c \"\\n\") );\n\n    // compilation time:\n    // \u003e\u003etime g++ -I ../include/ -std=c++14 -o test.o -c test.cpp\n    // real   0m1.176s\n    // user   0m1.051s\n    // sys    0m0.097s\n```\n\n#### More examples\n- [A rudimentary lazy TSV parser](https://godbolt.org/z/f6eptu).\n- [calendar.cpp](test/calendar.cpp) vs. [Haskell](https://github.com/BartoszMilewski/Calendar/blob/master/Main.hs) vs. [range-v3 implementation](https://github.com/ericniebler/range-v3/blob/master/example/calendar.cpp).\n- [aln_filter.cpp](test/aln_filter.cpp) for more advanced examples of use.\n\n### Description  \n\nUnlike `range-v3`, this library is centered around value-semantics rather than reference-semantics. This library does not know or deal with the multitude of range-related concepts; rather, it deals with data transformations via higher-order functions. It differentiates between two types of inputs: a `Container` and a lazy `seq\u003cNullaryInvokable\u003e` satisfying single-pass forward-only `InputRange` semantics (also known as a data-stream). Most of the function-objects in this library have two overloads of `operator()` respectively. Rather than composing views over ranges as with `range-v3`, `operator()`s take inputs by value, operate on it eagerly or compose a lazy `seq`, as appropriate (following the Principle of Least Astonishment), and return the result by value (with move-semantics) to the next stage.\n\nE.g.\n- `fn::where`\n  - given a container, passed by rvalue, returns the same container filtered to elements satisfying the predicate, using the erase-remove or iterate-erase idioms under the hood, as appropriate.\n  - given a container, passed by lvalue-reference, returns a copy of the container with elements satisfying the predicate, using `std::copy_if` under the hood.\n  - given a `seq`, passed by value, composes and returns a `seq` that will skip the elements not satisfying the predicate (lazy).\n- `fn::sort`\n  - given a container, passed by value, returns the sorted container.\n  - given a `seq`, passed by value, moves elements into a `std::vector`, and delegates to the above.\n- `fn::transform`\n  - given a `seq`, passed by value, returns a `seq` wrapping a composition of the transform-function over the underlying `NullaryInvokable` that will lazily yield the results of transform-function.\n  - given a container, passed by value, wraps it as `seq` and delegates to the above (i.e. also lazy).\n\n\nSome functions in this library internally buffer elements, as appropriate, with single-pass streaming inputs, whereas `range-v3`, on the other hand, imposes multipass ForwardRange or stronger requirement on the inputs in situations that would otherwise require buffering. This makes this library conceptually more similar to UNIX pipelines with eager `sort` and lazy `sed`, than to c++ ranges.\n\n| Operations | Buffering behavior | Laziness |\n| ---------- | ------------------ | -------- |\n| `fn::group_adjacent_by`, `fn::in_groups_of` | buffer elements of the incoming group | lazy |\n| `fn::unique_all_by` | buffer unique keys of elements seen so far | lazy |\n| `fn::drop_last`, `fn::sliding_window` | buffer a queue of last `n` elements | lazy |\n| `fn::transform_in_parallel` | buffer a queue of `n` executing async-tasks | lazy |\n| `fn::group_all_by`, `fn::sort_by`, `fn::lazy_sort_by`, `fn::reverse`, `fn::to_vector` | buffer all elements | eager |\n| `fn::take_last` | buffer a queue of last `n` elements | eager |\n| `fn::where_max_by`, `fn::where_min_by` | buffer maximal/minimal elements as seen so-far | eager |\n| `fn::take_top_n_by` | buffer top `n` elements as seen so-far | eager |\n\n\n### Signaling `end-of-sequence` from a generator-function\n\nMore often than not a generator-function that yields a sequence of values will not be an infinite Fibonacci sequence, but rather some bounded sequence of objects, either from a file, a socket, a database query, etc, so we need to be able to signal end-of-sequence. One way to do it is to yield elements wrapped in `std::unique_ptr` or `std::optional`:\n```cpp\n  fn::seq([]() -\u003e std::unique_ptr\u003c...\u003e { ... })\n% fn::take_while([](const auto\u0026 x) { return bool(x); })\n% fn::transform(fn::get::dereferenced{})\n% ...\n```\nIf your value-type has an \"empty\" state interpretable as end-of-inputs, you can use the value-type directly without wrapping.\n\nIf you don't care about incurring an exception-handling overhead once per whole seq, there's a simpler way of doing it: just return `fn::end_seq()` from the generator function (e.g. see my_intersperse example). This throws end-of-sequence exception that is caught under the hood (python-style). If you are in `-fno-exceptions` land, then this method is not for you.\n\n\n### Summary of different ways of passing inputs\n\n```cpp\n      fn::seq([]{ ... }) % ... // as input-range from a nullary invokable\n\n          std::move(vec) % ... // pass a container by-move\n                    vec  % ... // pass by-copy\n\n           fn::from(vec) % ... // as move-view yielding elements by-move (std::move will make copies iff vec is const)\n          fn::cfrom(vec) % ... // as above, except always take as const-reference / yield by copy\n           fn::refs(vec) % ... // as seq taking vec by reference and yielding reference-wrappers\n\nfn::from(it_beg, it_end) % ... // as a move-view into range (std::move will make copies iff const_iterator)\n  fn::from(beg_end_pair) % ... // as above, as std::pair of iterators\n```\nNote: `fn::from` can also be used to adapt an lvalue-reference to an `Iterable` that implements\n`begin()` and `end()` as free-functions rather than methods.\n\n\n### Primer on using projections\nGrouping/sorting/uniqing/where_max_by/etc. functions take a projection function rather than a binary comparator as in `std::` algorithms.\n```cpp\n\n    // Sort by employee_t::operator\u003c.\n    employees %= fn::sort(); // same as fn::sort_by( fn::by::identity{} );\n\n    // Sort by a projection involving multiple fields (first by last_name, then by first_name):\n    employees %= fn::sort_by L( std::make_pair( _.last_name, _.first_name ));\n\n    // The above may be inefficient (makes copies); prefer returning as tuple of references:\n    employees %= fn::sort_by L( std::tie( _.last_name, _.first_name ));\n\n    // If need to create a mixed tuple capturing lvalues as references and rvalues as values:\n    employees %= fn::sort_by L( std::make_tuple( _.last_name.size(), std::ref( _.last_name ), std::ref( _.first_name )));\n\n    // Equivalently, using fn::tie_lvals(...) that captures lvalues as references like std::tie, and rvalues as values:\n    employees %= fn::sort_by L( fn::tie_lvals( _.last_name.size(), _.last_name, _.first_name ));\n\n    // fn::by::decreasing() and fn::by::decreasing_ref() can wrap individual values or references,\n    // The wrapper captures the value or reference and exposes inverted operator\u003c.\n    // E.g. to sort by (last_name's length, last_name descending, first_name):\n    employees %= fn::sort_by L( fn::tie_lvals( _.last_name.size(), fn::by::decreasing_ref( _.last_name ), _.first_name ));\n\n    // fn::by::decreasing() can also wrap the entire projection-function:\n    employees %= fn::sort_by( fn::by::decreasing L( fn::tie_lvals( _.last_name.size(), _.last_name, _.first_name )));\n\n    // If the projection function is expensive, and you want to invoke it once per element:\n    auto expensive_key_fn = [](const employee_t\u0026 e) { return ... };\n\n    employees = std::move(employees)\n              % fn::transform([\u0026](employee_t e)\n                {\n                    auto key = expensive_key_fn(e);\n                    return std::make_pair( std::move(e), std::move(key) );\n                })\n              % fn::sort_by( fn::by::second{})   // or L( std::ref( _.second ))\n              % fn::transform( fn::get::first{}) // or L( std::move( _.first ))\n              % fn::to_vector();\n\n    // Alternatively, expensive_key_fn can be wrapped with a unary-function-memoizer\n    // (results are internally cached in std::map and subsequent lookups are log-time).\n    employees %= fn::sort_by( fn::make_memoized( expensive_key_fn ));\n\n    // fn::make_comp() takes a projection and creates a binary Comparator object\n    // that can be passed to algorithms that require one.\n    gfx::timsort( employees.begin(), employees.end(),\n                  fn::by::make_comp L( std::tie( _.last_name, _.first_name )));\n```\n\n### `#include` and compilation-time overhead\n\nDespite packing a lot of functionality, `#include \u003cfn.hpp\u003e` adds only a tiny sliver (~0.03s) of compilation-time overhead in addition to the few common standard library include-s that it relies upon:\n\n```cpp\n// tmp.cpp\n\n#if defined(STD_INCLUDES)\n#    include \u003cstdexcept\u003e\n#    include \u003calgorithm\u003e\n#    include \u003cfunctional\u003e\n#    include \u003cvector\u003e\n#    include \u003cmap\u003e\n#    include \u003cdeque\u003e\n#    include \u003cstring\u003e\n#    include \u003ccassert\u003e\n#elif defined(INCLUDE_FN)\n#    include \"fn.hpp\"\n#elif defined(INCLUDE_RANGE_ALL)\n#    include \u003crange/v3/all.hpp\u003e\n#endif\n\nint main()\n{\n    return 0;\n}\n```\n\n```sh\n# just std-includes used by fn.hpp\n\u003e\u003etime for i in {1..10}; do g++ -std=c++14 -DSTD_INCLUDES=1 -o tmp.o -c tmp.cpp; done\nreal    0m3.682s\nuser    0m3.106s\nsys     0m0.499s\n\n# fn.hpp\n\u003e\u003etime for i in {1..10}; do g++ -std=c++14 -DINCLUDE_FN=1 -o tmp.o -c tmp.cpp; done\nreal    0m3.887s\nuser    0m3.268s\nsys     0m0.546s\n\n# range/v3/all.hpp, for comparison\n\u003e\u003etime for i in {1..10}; do g++ -std=c++14 -DINCLUDE_RANGE_ALL=1 -I. -o tmp.o -c tmp.cpp; done\nreal    0m22.687s\nuser    0m20.412s\nsys     0m2.043s\n```\n\nThere are not many compiler-torturing metaprogramming techniques used by the library, so the template-instantiation overhead is reasonable as well (see the Top-5 most frequent word example; leaving it as an excercise to the reader to compare with raw-STL-based implementation).\n\n\n### Discussion\n\nMany programmers after getting introduced to toy examples get an impression that\nthe intended usage is \"to express the intent\" or \"better syntax\" or to \"separate the concerns\", etc.  \nOthers look at the toy examples and point out that they could be straightforwardly written\nas normal imperative code, and I tend to agree with them:\nThere's no real reason to write code like:\n```cpp\n    std::move(xs)\n  % fn::where(     [](const auto\u0026 x) { return x % 2 == 0; })\n  % fn::transform( [](const auto\u0026 x) { return x * x; }\n  % fn::for_each(  [](const auto\u0026 x) { std::cout \u003c\u003c x \u003c\u003c \"\\n\"; })\n```\n\n, if it can be written simply as\n```cpp\nfor(const auto\u0026 x : xs)\n    if(x % 2 == 0)\n        std::cerr \u003c\u003c x*x \u003c\u003c \"\\n\";\n```\nThe \"functional-style\" equivalent just incurs additional compile-time, debugging-layers, and possibly run-time overhead.\n\nThere are some scenarios where functional-style is useful:\n\n#### Const-correctness and reduction of mutable state.\nWhen you declare a non-const variable in your code (or worse, when you have to deal with an API that forces you to do that),\nyou introduce another \"moving part\" in your program. Having more things const makes your code more robust and easier to reason about.\n\nWith this library you can express complex data transformations as a single expression, assigning the result to a const variable\n(unless you intend to `std::move()` it, of course).\n\n```\nconst auto result = std::move(inputs) % stage1 % stage2 % ... ;\n```\n\nAnother code-pattern for this is immediately-executed-lambda.\n```cpp\nconst X x = [\u0026]\n{\n    X x{};\n\n    // build X...\n    return x;\n}();\n```\n\n#### Reduced boilerplate.\nE.g. compare\n```cpp\nemployees %= fn::where L( _.last_name != \"Doe\" );\n```\nvs. idiomatic c++ way:\n```cpp\nemployees.erase(\n    std::remove_if( employees.begin(), \n                    employees.end(), \n                    [](const employee_t\u0026 e)\n                    {\n                        return e.last_name == \"Doe\"; \n                    }), \n    employees.end());\n```\n\n#### Transformations over infinite (arbitrarily large) streams (`InputRange`s)\n\nThe most useful use-case is the scenarios for writing a functional pipeline\nover an infinite stream that you want to manipulate lazily, like a UNIX pipeline, e.g. the above-mentioned [aln_filter.cpp](test/aln_filter.cpp).\n\n#### Implement embarassingly-parallel problems trivially.\n\nReplacing `fn::transform` with `fn::transform_in_parallel` where appropriate may be all it takes to parallelize your code.\n\n### Downsides and Caveats.\nCompilation errors related to templates are completely gnarly.\nFor this reason the library has many `static_asserts` to help you figure things out. If you encounter a compilation error that could benefit from adding a `static_assert`, please open an issue.\n\n\nSometimes it may be difficult to reason about the complexity space and time requirements of some operations. There are two ways to approach this: 1) Peek into documentation where I discuss space and time big-O for cases that are not obvious (e.g. how `lazy_sort_by` differs from regular `sort_by`, or how `unique_all_by` operates in single-pass for `seq`s). 2) Feel free to peek under the hood of the library. Most of the code is intermediate-level c++ that should be within the ability to grasp by someone familiar with STL and `\u003calgorithm\u003e`.\n\n\nThere's a possibility that a user may instantiate a `seq` and then forget to actually iterate over it, e.g.\n```cpp\n    std::move(inputs) % fn::transform(...); // creates and immediately destroys a lazy-seq.\n\n    // whereas the user code probably intended:\n    std::move(inputs) % fn::transform(...) % fn::for_each(...);\n```\n\n### Minimum supported compilers: MSVC-19.15, GCC-4.9.3, clang-3.7, ICC-18\n\n### References:\n- [Haskell Data.List](https://hackage.haskell.org/package/base-4.12.0.0/docs/Data-List.html)\n- [Scala LazyList](https://www.scala-lang.org/api/2.13.x/scala/collection/immutable/LazyList.html)\n- [Elixir Stream](https://hexdocs.pm/elixir/Stream.html)\n- [Elm List](https://package.elm-lang.org/packages/elm/core/latest/List)\n- [O'Caml List](https://caml.inria.fr/pub/docs/manual-ocaml/libref/List.html)\n- [F# Collections.Seq](https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/sequences)\n- [D std.range](https://dlang.org/phobos/std_range.html)\n- [Rust Iterator](https://doc.rust-lang.org/std/iter/trait.Iterator.html)\n\nc++ -specific (in no particular order):\n- [c++20 std::ranges](https://en.cppreference.com/w/cpp/experimental/ranges)\n- [tcbrindle/NanoRange](https://github.com/tcbrindle/NanoRange)\n- [ericniebler/range-v3](https://github.com/ericniebler/range-v3)\n- [Dobiasd/FunctionalPlus](https://github.com/Dobiasd/FunctionalPlus)\n- [jscheiny/Streams](https://github.com/jscheiny/Streams)\n- [A Lazy Stream Implementation in C++11](https://www.codeproject.com/Articles/512935/A-Lazy-Stream-Implementation-in-Cplusplus)\n- [ryanhaining/CPPItertools](https://github.com/ryanhaining/cppitertools)\n- [soheilhy/fn](https://github.com/soheilhy/fn)\n- [LoopPerfect/conduit](https://github.com/LoopPerfect/conduit)\n- [k06a/boolinq](https://github.com/k06a/boolinq)\n- [pfultz2/linq](https://github.com/pfultz2/linq)\n- [boost.range](https://www.boost.org/doc/libs/1_67_0/libs/range/doc/html/index.html)\n- [cpplinq](https://archive.codeplex.com/?p=cpplinq)\n- [simonask/rx-ranges](https://github.com/simonask/rx-ranges)\n- [ReactiveX/RxCpp](https://github.com/ReactiveX/RxCpp)\n- [arximboldi/zug](https://github.com/arximboldi/zug)\n- [MarcDirven/cpp-lazy](https://github.com/MarcDirven/cpp-lazy)\n- [qnope/Little-Type-Library](https://github.com/qnope/Little-Type-Library)\n- [ftlorg/ftl](https://github.com/ftlorg/ftl)\n\nRecommended blogs:\n- [Eric Niebler](https://ericniebler.com)\n- [fluent c++](https://www.fluentcpp.com)\n- [Andrzej's C++ blog](https://akrzemi1.wordpress.com)\n- [foonathan::blog()](https://foonathan.net)\n- [quuxplusone](https://quuxplusone.github.io/blog)\n- [Bartosz Milewski](https://bartoszmilewski.com)\n","funding_links":[],"categories":["C++"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fast-al%2Frangeless","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fast-al%2Frangeless","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fast-al%2Frangeless/lists"}