joboccara / pipes

Pipelines for expressive code on collections in C++
MIT License
801 stars 80 forks source link

Allow pipelines to have a result (sink?) object #58

Closed fried-water closed 4 years ago

fried-water commented 4 years ago

I'm wondering if its possible to give pipelines a T sink() && function to return an arbitrary object upon completion. This would solve a number of issues such as allowing to create a collection on a single line, and implementing sink queries like count/contains/find/accumulate. This would require plumbing the return value through all the pipes at the end, as well as having the final pipeline essentially be an accumulator that holds some state.

At a first glance the easiest way to do this would be have the >>= operator for ranges and pipelines return sink() instead of void and adding a sink() overload to the pipelines.

// pipeline_base
auto sink() && { return void_t{}; } //  too bad we can't actually return void

// generic_pipeline
auto sink() && { return std::move(tailPipeline_).sink(); }

// fork
auto sink() && { return std::make_tuple(...); }

Then creating a count or to_vector sink pipeline would be fairly straightforward. Could also make a more generic to similar to what's being done with inserter.

template <typename Ele>
class vector_sink: public pipeline_base<vector_sink<T>>
{
public:
    template<typename T>
    void onReceive(T&& value) { vector_.push_back(FWD(value)); }

    auto sink() && { return std::move(vector_); }

private:
    std::vector<Ele> vector_;
};

I have no idea if any of the above compiles or their is some fundamental flaw in this approach, if not I can try to come up with a PR to try and fully implement it when I have some time.

joboccara commented 4 years ago

Thanks for the interesting idea. What would the call site look like for the user of the pipeline? To understand how sink() would be called.

fried-water commented 4 years ago

I'm imagining a user wouldn't ever need to call sink() directly, its just an api function for terminal pipelines like onReceive(). Just take advantage of the return slot of operator>>=(Range, Pipepline) which currently returns void.

So from your first example:

std::vector<int> destination = source >>= pipes::filter([](int i){ return i % 2 == 0; })
       >>= pipes::transform([](int i){ return i * 2; })
       >>= pipes::to_vector<int>();

A big downside of this compared to a pull model is you lose the ability to short circuit, so terminal pipelines like find() wouldn't ever be optimal. But accumulate is possible (which is enough to implement everything).

Edit: Thinking about it a bit more you could add short circuiting through some back pressure mechanism, but it wouldn't be straightforward.

godefv commented 4 years ago

Note: This is related to issue #33.