After taking the time to understand the existing proposals more fully I am ready to begin talking about the minimal changes to the exisiting proposals that would define concepts that
- enables all the existing functionality a. execute b. twoway_execute c. via_execute (proposed to replace then_execute) d. bulk_execute (still have work to do here)
- adds error propagation (errors can still be delegated to the executor explicitly)
- adds uniform composibility (executors and futures compose using the same adaptor concept and pipe implementation)
- enables lazy submission without specifying undefined-behavior when promise.set_value is called before future.then
- does not specify the property system (will work with requires/query or with a compile-time only property system)
NOTE: then and transform are interchangeable in this discussion.
template<class Function, class Future>
std::experimental::future<result_of_t<decay_t<Function>(decay_t<Future>)>>
then_execute(Function&& f, Future&& pred) const;
then_execute has been described to me as
- required to allow an executor to chain dependent tasks efficiently.
- designed to be the implementation of future.then
The ramifications of this design have meant that
- an executor is bound to its future implementation. This binding makes it complicated to interoperate with other future implementations and complicates transition from one executor to another.
- a future is bound to an executor. In order to call then_execute in the .then implementation the future implementation must have access to an executor instance.
- future.then defaults to a new task being queued, unless the executor makes an effort to optimize these away by an executor specific heuristic.
- users must assume that each .then will spawn a new task and when they wish to prevent a new task between two function calls they must structure the function passed to future.then to run more code.
- Note that future.via also specifies the creation of a new task by default.
There is another way to allow an executor to discover dependent work without these ramifications.
template<Sender S>
auto via_execute(S pred);
via_execute is very similar to then_execute.
- takes a future and returns a future.
- as the name indicates, used to implement via just like then_execute was used to implement then
The ramifications of via_execute
- then is just a function call - no task is ever created. a set of chained then calls are collapsed by the compiler, not manually by the user.
- via's behaviour is unchanged.
- users have clear control over when they would like to introduce new tasks by using via with the same or a different executor (with the executor still being allowed to optimize away new tasks using the via_execute customization point)
- users have clear control over when they want to prevent new tasks by using then.
- there is no need to bind future to an executor. via is explicitly passed an executor.
concepts.h defines the following set of concepts using concepts-lite.
concepts.h also defines the customization points.
concepts.h destructures these concepts to build the full concepts by combining elemental concepts that would not be implemented in isolation.
The destrucuturing makes concepts.h appear to have many concepts when only the following fully assembled concepts are the surface that is implemented.
- NoneReceiver aka
promise<void>
struct NoneReceiver {
void done();
template<class E>
void error(E) noexcept;
};
- SingleReceiver aka
promise<T>
(subsumes NoneReceiver)
struct SingleReceiver {
void done();
template<class E>
void error(E) noexcept;
template<class V>
void value(V);
};
- NoneSender aka
future<void>
struct NoneSender {
template<None R>
void submit(R);
};
- SingleSender aka
future<T>
andExecutor
(subsumes NoneSender)
struct SingleSender {
template<Single R>
void submit(R);
};
- Adaptor - A function to chain senders
struct Adaptor {
template<Sender S>
Sender adapt(S);
};
- Lifter - A Function to chain receivers
struct Lifter {
template<Receiver R>
Receiver lift(R);
};
- Via (proposed to replace SemiFuture)
struct Via {
template<Executor Ex>
Sender via(Ex);
};
- ViaExecutor (proposed to replace
then_execute
)
struct ViaExecutor {
template<Sender S>
Sender via_execute(S);
};
- BulkExecutor
struct BulkExecutor {
template<Sender S, class Op, class ShapeF, class StateF, class ResultS>
Sender operator()(S, Op, ShapeF, StateF, ResultS);
};
Each of the concepts needs a type-erasure definition in std.
Each of the concepts needs a 'builder' definition in std.
A builder type makes the creation of new implementations of each concept easy. We have all suffered because iterator has no builder types in std.
This is a Toy builder type
template<class Data, class ValueFn>
struct my_promise {
Data data_;
ValueFn valuefn_;
template<class V>
void value(V v) {
valuefn_(data_, v);
}
template<class E>
void error(E) {
std::abort();
}
void done() {
std::abort();
}
};
template<class Data, class ValueFn>
my_promise(Data, ValueFn) -> my_promise<Data, ValueFn>;
The Toy builder type is used to build new Toy implementations
template<class T>
auto get(){
return my_adaptor{[](auto in){
T result;
::mc::submit(in, my_promise{&result, [](T* r, auto v){ *r = v; }});
return result;
}};
}