Basics

Operations

This is a simple overview of how to use pipedream operations (the term that will generally be used for something on the right hand side of |). These generally consist of:

  • A simple immediate value, which can be modifying or not, such as replace or erase_if
  • A generator that takes and/or produces values

In general, operations should return either a new value, or a reference to the original value, to readily support chaining.

Immediate Operations

The simplest operations are immediate: that is, they take a value, and return a value. For instance, split is an immediate operation:

auto s = "foobar" | replace("bar","baz");

Other operations may modify the value, as in the case of sort:

std::vector<int> v{1,5,3,7,-22,1,10};
v | sort;

This may seem odd at first, but remember: pipedream considers | to be a general form of .. Thus, like member functions, operations may be modifying or not, and are documented as such.

For operations such as replace that produce a new value of the same type, which may be assigned back using |=:

std::string s = "abc";
s |= replace(".b", "xb");

Generator Operations

Generators are, strictly speaking, operations that produce values on demand with next(), and may be used in range-based for. For example:

for(auto i : from_to(0,5))
    std::cout << i << std::endl;

In this case, from_to is a generator that produces values in the half-open range from 0 to 5.

Some operations consume input from generators:

auto v = from_to(0,5) | collect<std::vector>;

Note that collect is not a generator: it's an immediate operation that produces a std::vector by consuming input from a generator. Of course, because chaining operations is very useful, generators can produce and consume values:

auto v = from(0) | take(3) | collect<std::vector>;

In this case take(n) will produce the first n values, and then is empty.

Getting Things Done

So, by now you have likely seen examples, and want to know what you can really do and how to do it. For the most part, the API reference can help you here; it's organized by some broad categories, and this is a small-but-growing library.

You may also wonder how you can [build your own], which you can read about there.

Here, we will cover a few things to get you started.

Standard Containers

You may try something like this. It won't work:

std::vector<int> v{1,2,3,4,5};

// Error:
auto v2 = v | take(3) | collect<std::vector>;

This is because std::vector is not a generator. Instead, for containers, we need to "adapt" them into a generator chain. This is done with each:

std::vector<int> v{1,2,3,4,5};

// OK
auto v2 = v | each | take(3) | collect<std::vector>;

Vectors aren't the only container that works:

// examples/basic_stdmap1.cpp

#include <iostream>
#include <map>
#include <string>
#include <piped.hpp>

using namespace piped;

int main() {
    std::map<std::string, std::string> m{{"foo", "oof"}, {"bar", "rab"}, {"baz", "zab"}};

    for(auto p : m | each | take(2)) {
        std::cout << p.second << std::endl;
    }
}

collect

One of the more "hard to put down" operations used througout the documentation is collect<C>:

auto v = from_to(0,10) | collect<std::vector>;

This is of course very flexible and direct for constructing container values. It too is not limited to std::vector; let's pull the front page example apart a bit:

auto key_g   = "a,b,c" | split(",");
auto value_g = from(1);

auto m = zip(key_g, value_g) | collect<std::map>;

In this case, collect<std::map> specifically builds a map from the first two entries of a std::tuple. This of course is exactly what zip generates!

But, what exactly does zip provide?

zip

As it happens, zip is a "generator of a bunch of generators". It's not limited to two inputs:

// examples/basic_zip1.cpp

#include <iostream>
#include <tuple>
#include <vector>
#include <piped.hpp>

using namespace piped;

int main()
{
    auto v = from_to(0,100) | collect<std::vector>;

    auto g = zip(from(1),
                 from(10, 5),
                 v | each);

    for(auto [a,b,c] : g) {
        std::cout << a << " "
                  << b << " "
                  << c
                  << std::endl;
    }
}

This generates a std::tuple from each generator given as a parameter, which is handy for a number of things, such as the above destructuring.

You will also notice that we use two from generators here: these are unbounded, but we don't get an infinite loop (and a crash). This is because zip only generates values until any of the generators are empty, then it is empty in turn.

You may wonder, "but what if I want to iterate all the values of uneven containers, until the end of the largest?"

This is possible: you could make a generator that returned first its input then some default or empty value. It will be implemented in pipedream as soon as I encounter a real use case. So far after years of using similar constructs, I have not.

erase_if and filter

You are likely familiar with the [erase-remove idiom]. If you are like most, you find this annoying an error-prone. Pipedream, of course, lets you simply do the following:

auto v = from_to(0,10) | collect<std::vector>;

v | erase_if([](auto x) { return x < 5; });

This is a modifying operation; if you want a copy, you can use the filter generator; but note, the test is reversed:

auto v = from_to(0,10) | collect<std::vector>;

auto v2 = v | each
  | filter([](auto x) { return !(x < 5); })
  | collect<std::vector>;

Splitting and Joining Strings

These common operations should be simple; with pipedream, they are:

auto v = "foo,bar,baz" | split(",") | join(":") | collect<std::vector>;

You may use a std::regex for split, or a regular string.

Note

split produces std::string_view. Therefore, this makes a std::vector<std::string_view>. This is normally almost certainly what you want... unless you're splitting a temporary string.

If you want to make copies, you might do something like the following:

// examples/basic_split1.cpp

#include <iostream>
#include <string>
#include <piped.hpp>

using namespace piped;

int main()
{
    auto v = "foo,bar,baz:quux"
        | split(",")
        | join(":")
        | split(":")
        | map_to<std::string>
        | collect<std::vector>;

    for(auto&& s : v)
        std::cout << s << std::endl;
}

This is rather contrived, but if you don't map_to<std::string> here, you will end up with a vector of dangling pointers. But this is very easy to do as we wish, instead.