Promise is a concept with many implementations
Perhaps one of the reasons that there are so many promise
proposals is that each is an implementation of the same Concepts with different tradeoffs.
A
promise
will transport a single value across time.
To provide motivation for a Concept based approach to promise
standardization, I will compare and contrast promise
with the STL.
Multiple implementations of Containers and Iterators were included in the Standard. This invocation of the Rule of Twos, resulted in robust and stable concepts that have been used to successfully implement additional containers, iterators and algorithms outside of the Standard.
implementations
The different promise
implementations are similar to vector
, list
and string
.
- some will block with
get()
- some will poll on a thread and invoke the
then()
callback - some will invoke the
then()
callback directly from the context of the producer of the value
Perhaps string
is the most unfortunate comparison, since like string
, the current promise
proposals embed some algorithms into the implementation. The current promise
implementations also embed execution context support into the implementation.
Specifying Concepts instead of implementations will naturally extract algorithms and execution context support from the promise
implementations so that these can be shared across multiple promise
implementations.
algorithms
In the current proposals some algorithms are embedded into the promise implementations (eg. then()
) and others (eg. when_any()
& when_all()
) are external.
In the current proposals then()
is overloaded to mean both transform()
and transform_flatten()
(aka flat_map()
, map() | merge()
, etc..)
With the right Concepts, then()
, get()
, when_any()
and when_all()
will all be implementations of the same Concepts.
With the right Concepts, additional algorithms - delay()
, produce_on()
, consume_on()
, finally()
, error()
, retry()
, tap()
will be implemented.
With the right Concepts, users can build any algorithm that they wish.
execution context
There are two places in a promise where control over the execution context is desired. One is the producer of the value, the other is the consumer of the value.
With the right Concepts for promise, these are just additional algorithms.
The produce_on(Context)
algorithm is a promise implementation that queues the producer callback onto the provided context.
The consume_on(Context)
algorithm is a promise implementation that queues the then()
callback onto the provided context.
The true power of moving these out of the promise
implementation is that overloads of the algorithms for different execution contexts can coexist and compose.
I intentionally avoided using Executor since with Concepts there would be no coupling between a
promise
implementation and whatever Executor turns out to be. Overloads of the above algorithms can be added over time as Executor evolves.
negative space
The negative space between various Container implementations and the set of useful algorithms, exposed the shape of the several Iterator categories. Similar to the Rule of Twos this increases confidence in the completeness and correctness of the Concepts.
A set of Concepts that fill the negative space between multiple promise
implementations and the set of useful algorithms, will not look the same as the current proposals. However, all the current proposals could be implemented with these Concepts
Single
Single and its companions are Concepts that will transport a value across time.
Single and its companions fill the negative space between producers, consumers and the set of useful algorithms.
Single is used both to produce the value and to consume the value.
- A Single is passed to the producer which will call
value()
orerror()
- A Single is created by
then()
that calls the lambda whenvalue()
is called.
Lifetime is a Concept that will scope the data and state across time.
SingleSubscription is a Concept that binds a consumer to the Lifetime scope.
SingleDeferred is a Concept that binds the producer and the consumer and the scope, invokes the producer, transports the value from the producer to the consumer and ends the scope.
I am still trying to correctly describe these using concepts-lite. I need a way to allow
single::value()
andsingle::error()
to leave their arguments unconstrained.
template <typename L>
concept bool Lifetime() {
return requires(const L& l) {
{ l.is_stopped() } -> bool;
{ l.stop() } -> void;
};
}
template <typename S>
concept bool Single() {
return requires(const S& s) {
{ s.value(auto) } -> void;
{ s.error(auto) } -> void;
};
}
template <typename S>
concept bool SingleSubscription() {
return requires(const S& s) {
requires Lifetime<s.lifetime>;
requires Single<s.destination>;
};
}
template <typename S, typename D>
concept bool SingleDeferred() {
return requires(const S& s, const D& d) {
requires Lifetime<s.subscribe(d)>;
};
}
struct alifetime
{
bool is_stopped() const;
void stop() const;
};
static_assert(Lifetime<alifetime>(), "not a Lifetime");
struct asingle
{
template<typename T>
void value(T&& t) const;
template<typename E>
void error(E&& e) const;
};
static_assert(Single<asingle>(), "not a Single");
struct asinglesubscription
{
alifetime lifetime;
asingle destination;
};
static_assert(SingleSubscription<asinglesubscription>(), "not a SingleSubscription");
struct asingledeferred
{
Lifetime subscribe(SingleSubscription) const;
};
static_assert(SingleDeferred<asingledeferred>(), "not a SingleDeferred");
still to come..
I hope this whets the appetite for upcoming explorations of how to implement a promise
and the then()
, produce_on()
and consume_on()
algorithms using these concepts.
In the fullness of time, the hint in the name Single will be confirmed by the introduction of Legion
My name is Legion .. because we are many
references
acknowledgements
This is informed by many discussions and prior art in the reactivex and C++ communities, as well as conversations with; Bryce Lelbach, David Sankel, Gor Nishanov and many others..