Add subsections to the "Design - User side" section describing sender/awaitable interop#181
Conversation
std_execution.bs
Outdated
| In truth there will be no problem because all awaitable types automatically model the `typed_sender` concept. The adaptation is transparent and happens in the sender customization points, which are aware of awaitables. | ||
|
|
There was a problem hiding this comment.
"all awaitable types"
You need to be careful and precise what you mean when you say "awaitable" here.
I found there was much confusion about the subtle nuances in what we mean when we say "awaitable".
There can be awaitable types that are only awaitable within certain contexts.
e.g. many of the folly::coro awaitable types extract some information from the awaiting coroutine's promise via the coroutine_handle passed to await_suspend(). You can't just pass any old coroutine_handle to such an awaitable and expect it to work here.
In the P1288 paper the awaitable concept required that await_suspend() was invocable with coroutine_handle<void> as a proxy for accepting any coroutine_handle and thus being awaitable within any natural coroutine-context (i.e. without await_transform).
| } | ||
| ``` | ||
|
|
||
| Since awaitables are senders, writing a sender-based asynchronous algorithm is trivial if you have a coroutine task type: implement the algorithm as a coroutine. If you are not bothered by the possibility of allocations and indirections as a result of using coroutines, then there is no need to ever write a sender, a receiver, or an operation state. |
There was a problem hiding this comment.
there is no need to ever write a sender
This comes with the proviso that there are sufficient concurrency building-blocks available that you can use.
Not all algorithms can be implemented purely with coroutines. Having said that, most algorithms can be written using coroutines once you have something like async_scope that can spawn operations that execute concurrently - allowing structured concurrency algorithms to be built out of the ad-hoc concurrency mechanisms.
std_execution.bs
Outdated
| <pre highlight="c++"> | ||
| template <class S> | ||
| requires <i>single-typed-sender</i><S&> // See <a href="#spec-execution.coro_utils.as_awaitable">[execution.coro_utils.as_awaitable]</a> | ||
| task<<i>single-sender-value-type</i><S>> retry(S& s) { |
There was a problem hiding this comment.
Should this be pass-by-value to avoid the dangling-reference problems?
This algorithm would be difficult to use in a sender composition expression as it won't accept temporary senders as arguments.
std_execution.bs
Outdated
|
|
||
| When your task type's promise inherits from `with_awaitable_senders`, what happens is this: the coroutine behaves as if an *uncatchable exception* had been thrown from the `co_await` expression. (It is not really an exception, but it's helpful to think of it that way.) Provided that the promise types of the calling coroutines also inherit from `with_awaitable_senders`, or more generally implement a member function called `unhandled_done`, the exception unwinds the chain of coroutines as if an exception were thrown except that it bypasses `catch(...)` clauses. | ||
|
|
||
| In order to "catch" this uncatchable done exception, one of the calling coroutines in the stack would have to await a sender that maps the done channel into either a value or an error. That is achievable with either the `execution::let_done` or `execution::upon_done` sender adaptors. |
There was a problem hiding this comment.
I'm not sure that we should be recommending let_done here.
It's use within a coroutine is complicated as having the let_done() return a just(std::nullopt_t) will result in the overall sender having multiple result-types and then needing to be piped through into_variant.
It would be nicer if we could describe an done_as_optional algorithm and show an example of its use...
std::optional<T> result = co_await done_as_optional(some_operation(args...));
if (!result) {
// completed with set_done()
} else {
// completed with set_value()
auto& value = *result;
}Whereas doing the same thing with let_done() is more cumbersome:
std::optional<T> result = co_await transform(
let_done(some_operation(args...), []() noexcept { return just(std::nullopt); }),
[](auto&& value) -> std::optional<T> { return static_cast<decltype(value)>(value); });Maybe also throw in an example of using done_as_error<E>()?
try {
T result = co_await done_as_error<operation_cancelled>(some_operation(args...));
} catch (operation_cancelled) {
...
}Or alternatively show that you can just let this exception propagate up through the call-stack of coroutines...
…r-interop # Conflicts: # include/execution.hpp # std_execution.bs
fixes #167