Closed brycelelbach closed 11 months 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
.
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)
.
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.
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
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.
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
see also select
in #940
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:
Today, I instead have to write something like:
which I find quite inelegant.