NVIDIA / stdexec

`std::execution`, the proposed C++ framework for asynchronous and parallel programming.
Apache License 2.0
1.64k stars 166 forks source link

Sending multiple values from `then` #218

Closed brycelelbach closed 11 months ago

brycelelbach commented 3 years ago

I'd like to have a way to have the result of a function passed to then sent as multiple values. I've come across the need for this multiple times.

E.g. I want to be able to do something like this:

  then([] (T t) { return some_magic{t, U{}}; })
| then([] (T t, U u) { /* ... */ })

Today, I instead have to write something like:

  then([] (T t) { return std::tuple{t, U{}}; })
| then([] (std::tuple<T, U> t) { auto [t, u] = t; /* ... */ })

which I find quite inelegant.

brycelelbach commented 3 years ago

@mjgarland suggested that perhaps this could be a new adaptor:

t | something(u) | then([] (T t, U u) { /* ... */})

Where something is like just, except it passes along the predecessor's value too.

Perhaps we could call it also.

lewissbaker commented 3 years ago

There are potentially a couple of options here. One is to extend then() to support accepting a number of function-objects, each producing a different output value.

then(src, fs...)

produces a sender that transforms the a result from src of set_value(r, values...) into into set_value(parent_r, fs(values...)...).

Another option is to define a new algorithm, say then_with(src, f) that maps a completion of set_value(r, values...) to set_value(parent_r, values..., f(values...)).

Other algorithms we could consider adding are: unpack_tuple(src) that transforms set_value(r, some_tuple) to std::apply([&](auto&&... values) { set_value(parent_r, values...); }, some_tuple) or a more general unpack_tuples that basically does a tuple_cat on the values followed by unpack_tuple(src).

kirkshoop commented 3 years ago

I would expect this to be done using let_value()

Anything else would just be convenience

That said, for the convenience algo, I would prefer the accretion model to the pack of functions model.

miscco commented 3 years ago

From my point of view, then is already a CPO, so we can specify it to use apply to flatten out the passed tuple before trying to invoke by passing a single argument.

There would obviously be an issue with an API that has overloads that take a tuple and some other mutliple args, but I would assume that in the case of then where we expect lambdas to be passed

kirkshoop commented 3 years ago

we can specify it to use apply to flatten out the passed tuple

This is an example of something that I would call "auto-magic".

I have found that auto-magic solutions make it harder for users to express what they want.

In rxcpp I had a util called apply_to(). apply_to is a function object that takes a tuple and applies it to the stored function passed in the constructor. When used with an algo it would allow the user to explicitly apply a tuple.

.. | map(apply_to([](auto&&... values){..}) ..

As you say, packs of values where one arg is a tuple become problematic. Another is nested tuples (how deep should the unwrapping go?). Another is other tuple-like types (is only std::tuple expanded? Is any type that supports get<> and tuple_size going to be expanded?). The user has to reason about this in order to predict the arguments that will be passed to their function. The user has to reason about this in order to prevent the expansion in cases where they need the tuple as is (do they wrap it in a variant or optional or a custom type to hide the tuple from the expansion?).

Once tuple is expanded, why not optional and variant? They would just expand the value into a call to an overload set. What about if an overload that matched an expanded optional or variant was already used for a different purpose than the explicit optional and variant cases? This expansion would then mess with the semantics.

In my experience "auto-magic" increases the burden for users even when it is intended to reduce the burden for users. There are other ways to allow users to easily and explicitly ask for expansion when that is what they want.

miscco commented 3 years ago

Yeah, I totally agree that this is a sharp edge and we should be both clear and careful in how we define the contract.

My point was mainly that the implementation burden on flattening tuples for function calls is rather low and because then is a CPO we can define the way it works without too much trouble.

Maybe the solution would be to define then_apply and then_invoke

ericniebler commented 11 months ago

see also select in #940