SkillAgentSearch skills...

LazyCode

C++14: bringing in those expressions that you wish you had, Lazily evaluated, compossible generators, maps, filters, ranges and more...

Install / Use

/learn @SaadAttieh/LazyCode
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Lazy Code

Cute and Efficient Is Definitely Possible

  • Compossible, lazily evaluated generators such as map, filter, fold, enumerated ranges and more.
  • Easy handling of input and output streams, reading line by line, parsing objects, etc.
  • Compact syntax: Choice between
    • Functional fold(...,map(...,filter(...)))
    • Piped filter(...) | map(...) | fold(...)
  • Zero cost abstractions leveraging C++ templates, no macros involved.
  • Easily create new generators and integrate with existing ones.
  • Optionally enabled macros to make the syntax even more cute.

Quick examples:

This is just some demonstrations of the coding style enabled with this library. A full listing comes after the examples.

namespace lz = LazyCode;
using namespace std;

Example 1:

  • Read in lines of text into a vector until EOF is reached, one line per vector element.
  • Sort the vector and then reprint the lines.
    auto lines = lz::readLines(cin) | lz::append(vector<string>());
    sort(lines.begin(), lines.end());
    lz::generator(lines) | lz::write(cout, "\n");

Yup, that's it. And it gets better...

Example 2:

  • Read in up to 10 integers from a file "test.txt".
  • filter for the even numbers, square them and sum their values.
  • Do not store the numbers in a container, after all, it could easily be more than 10 numbers.
    int total = lz::read<int>(ifstream("test.txt")) | lz::limit(10) |
                lz::filter([](int i) { return i % 2 == 0; }) |
                lz::map([](int i) { return i *  i; }) | lz::sum();

Wow, that's compact. Maybe too compact? If you are concerned, you can split that line up into multiple expressions. Take a look:

    auto numbers = lz::read<int>(ifstream("test.txt")) | lz::limit(10);
    auto evenFilter = numbers | lz::filter([](int i) { return i % 2 == 0; });
    auto squares = evenFilter | lz::map([](int i) { return i *  i; });
    int total = squares | lz::sum();
  • Even though this expression is split over multiple variable assignments, it is not any less efficient.
  • Each intermediate variable simply describes a unit of code to be executed. All held in stack. Nothing is executed until the final pipe into the sum().
  • The final value (the sum of the squares of only even numbers in a file) is calculated in a single pass with no intermediate container or memory allocations required.
  • for each number in test.txt, evaluate filter condition, and add to total.

Use a functional style instead:

Piping does not work with you? Simply use the functional interface:

    auto numbers = lz::limit(10, lz::read<int>(ifstream("test.txt")));
    auto evenFilter = lz::filter([](int i) { return i % 2 == 0; }, numbers);
    auto squares = lz::map([](int i) { return i *  i; }, evenFilter);
    int total = lz::sum(squares);

Even more cute:

Those long lambdas, what can we do? You could use a macro * don't panic* , macros are optional, I only offer a single one for convenience.

    int total = lz::read<int>(cin) | lz::limit(10) |
                lz::filter(lambda(i, i % 2 == 0)) | lz::map(lambda(i, i *  i)) |
                lz::sum();

The lambda macro (* if you want it* ) is there to build the standard lambda, one that captures the surrounding context and that can take both lvalue and references. It simply uses all but the last argument as parameter names and the last argument as the return expression to evaluate. lambda(a,b,c,expression) maps to [&] (auto&& a, auto&& b, auto && c) { return expression; }.

It can be disabled_ by defining #define LAZY_CODE_NO_MACROS before including the lazyCode header.

A case for safety:

Writing the equivalent of the above in plain old c++ is more cumbersome and can be argued to be less safe. For example, this almost equivalent snippet has a bug. Can you spot it?

    int total = 0;
    for (int i = 0; i < 10; i++) {
        int value;
        cin >> value;
        if (value % 2 == 0) {
            total += value *  value;
        }
    }

The bug? What if the user enters less than 10 numbers. You'll be reading EOF symbols into your total. Silly example, but the idea is there. This is not only more compact and readable, it can enable safer code.

Installation:

  • A c++14 or above compiler is required.
  • For your convenience, a single header is maintained in the repo's top level directory single_header. LazyCode can therefore be used just by including this single header, single_header/lazyCode.h.
  • Alternatively, the project is built as a standard Cmake header only interface and can be included into a cmake project via cmake's add_subdirectory.
  • For contributers, notes on how to recreate the single header after a source change is described at the end of this readme.

Method 1: include the single header (easiest)

  1. Download the single header here
  2. Include it from any c++ file: #include "path_to_lazyCode.h"

or, to stay up to date with the latest release, consider adding lazyCode as a sub module:

git submodule add https://github.com/SaadAttieh/lazyCode

Method 2: as a cmake sub directory.

#add lazyCode as a submodule:
git submodule add https://github.com/SaadAttieh/lazyCode
#Tell git to download submodules of lazyCode
git submodule init ; git submodule update

Then add this to your cmake file:

add_subdirectory (lazyCode)

The docs:

The Basics:

There are two types of objects that can be created, generators and collectors.

A generator is simply a state object paired with a function that can be called to produce values based off the state. This is discussed later in the section creating your own generators. A generator by itself does not execute any instructions. This is why we call it lazy evaluation; it merely describes how values may be generated. Values may be pulled from a generator via the next() function;, by using a for loop for (auto i: generator), or by passing it to a collector. Collectors pull values from a generator and perform a given action, see the section below on collectors.

Basic generators:

range:

template <typename Number, typename std::enable_if<
                               std::is_integral<Number>::value, int>::type = 0>
auto range(Number end);
  • Create a generator of sequence of integral values. Sequence begins at 0 (inclusive) , is followed by values increasing by 1 and stops at the specified last point (exclusive).
  • range(5) generates 0,1,2,3,4,
<!-- -->
template <typename Number, typename std::enable_if<
                               std::is_integral<Number>::value, int>::type = 0>
auto range(Number start, Number end); 
  • Create a generator of sequence of integral values. Sequence begins at specified start point (inclusive) , is followed by values increasing by 1 and stops at the specified last point (exclusive).
  • range(2,5) generates 2,3,4
<!-- -->
template <typename Number1, typename Number2>
auto range(Number1 start, Number1 end, Number2 increment); 
  • Create a generator of sequence of values. Sequence begins at specified start point (inclusive) , is followed by values increasing/decreasing by the specified increment and stops at the specified last point (exclusive).
  • range(0.1,1.0,0.2) generates 0.1,0.3,0.5,0.7,0.9
<!-- -->

infRange

template <typename Number1, typename Number2>
auto infRange(Number1 start, Number2 increment)
  • Create a never ending generator of sequence of values. Sequence begins at specified start point (inclusive) and is followed by values increasing/decreasing by the specified increment.
  • infRange(0,2) infinite range, generates 0,2,4,6,8,...
<!-- -->

readLines

template <typename Stream>
auto readLines(Stream&& stream)
  • return a generator that reads lines from the given stream. The generator yields a new string for each line. If an lvalue is given, only a reference to the stream is held. If a rvalue is given, the generator takes ownership, the stream is moved into the generator.
  • readLines(cout) reads from std::cout, only a reference to cout is held
  • readLines(istringstream(someString)) reads from the newly created string stream, the string stream is moved into the generator.
<!-- -->

read

template <typename ReadType, typename Stream>
auto read(Stream&& stream)
  • return a generator that reads from the given stream. The generated type (the type of values pulled from the stream) must be specified as the first template parameter. For example, to read integers from the stream, use read<int>. If an lvalue is given, only a reference to the stream is held. If a rvalue is given, the generator takes ownership, the stream is moved into the generator.
  • read<double>(cin) read double values from cin.
<!-- -->

generator

template <typename Container>
decltype(auto) generator(Container&& container)
  • Create a generator from a container. The generator uses the containers begin and end iterators via std::begin, std::end. If an rvalue is given, the generator will take ownership of the container and move it into the generator object, otherwise the generator will only hold a reference to the container.
  • generator(v) v can be a vector, map, set, anything with begin/end iterators. Only a reference to v is held.
  • generator(V()) V can be a vector, map, set, anything with begin/end iterators. Since it is a newly created container, it is moved into the generator.
<!-- -->

slice

template <typename Container>
decltype(auto) slice(Container&& container, size_t start, size_t last)
  • return a generator that iterates through a container from position start (inclusive) to position end (exclusive). If an rvalue is given, the generator will take
View on GitHub
GitHub Stars155
CategoryDevelopment
Updated1mo ago
Forks7

Languages

C++

Security Score

80/100

Audited on Feb 23, 2026

No findings