cplusplus / sender-receiver

Issues list for P2300
Apache License 2.0
20 stars 3 forks source link

Sending multiple values from `then` #133

Open ericniebler opened 10 months ago

ericniebler commented 10 months ago

Issue by brycelelbach Wednesday Oct 20, 2021 at 16:04 GMT Originally opened as https://github.com/NVIDIA/stdexec/issues/218


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.

ericniebler commented 10 months ago

Comment by brycelelbach Wednesday Oct 20, 2021 at 16:09 GMT


@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.

ericniebler commented 10 months ago

Comment by lewissbaker Friday Oct 22, 2021 at 13:04 GMT


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).

ericniebler commented 10 months ago

Comment by kirkshoop Thursday Nov 25, 2021 at 05:57 GMT


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.

ericniebler commented 10 months ago

Comment by miscco Thursday Nov 25, 2021 at 07:27 GMT


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

ericniebler commented 10 months ago

Comment by kirkshoop Thursday Nov 25, 2021 at 14:46 GMT


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.

ericniebler commented 10 months ago

Comment by miscco Thursday Nov 25, 2021 at 15:56 GMT


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 10 months ago

Comment by ericniebler Thursday Dec 07, 2023 at 23:27 GMT


see also select in #940