Skip to content

Instantly share code, notes, and snippets.

@kirkshoop
Last active August 2, 2018 00:59
Show Gist options
  • Save kirkshoop/f743929f16dd2ba2f89f8d3fe3e10d42 to your computer and use it in GitHub Desktop.
Save kirkshoop/f743929f16dd2ba2f89f8d3fe3e10d42 to your computer and use it in GitHub Desktop.
minimal concepts for P0443 and P1054

Minimal concepts

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

  1. 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)
  2. adds error propagation (errors can still be delegated to the executor explicitly)
  3. adds uniform composibility (executors and futures compose using the same adaptor concept and pipe implementation)
  4. enables lazy submission without specifying undefined-behavior when promise.set_value is called before future.then
  5. does not specify the property system (will work with requires/query or with a compile-time only property system)

then_execute

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

  1. required to allow an executor to chain dependent tasks efficiently.
  2. designed to be the implementation of future.then

The ramifications of this design have meant that

  1. 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.
  2. 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.
  3. future.then defaults to a new task being queued, unless the executor makes an effort to optimize these away by an executor specific heuristic.
  4. 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.
  5. 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.

  1. takes a future and returns a future.
  2. as the name indicates, used to implement via just like then_execute was used to implement then

The ramifications of via_execute

  1. 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.
  2. via's behaviour is unchanged.
  3. 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)
  4. users have clear control over when they want to prevent new tasks by using then.
  5. there is no need to bind future to an executor. via is explicitly passed an executor.

concepts.h

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.

Promise concepts

  1. NoneReceiver aka promise<void>
struct NoneReceiver {
    void done();
    template<class E>
    void error(E) noexcept;
};
  1. SingleReceiver aka promise<T> (subsumes NoneReceiver)
struct SingleReceiver {
    void done();
    template<class E>
    void error(E) noexcept;
    template<class V>
    void value(V);
};

Future concepts

  1. NoneSender aka future<void>
struct NoneSender {
    template<None R>
    void submit(R);
};
  1. SingleSender aka future<T> and Executor (subsumes NoneSender)
struct SingleSender {
    template<Single R>
    void submit(R);
};
  1. Adaptor - A function to chain senders
struct Adaptor {
    template<Sender S>
    Sender adapt(S);
};
  1. Lifter - A Function to chain receivers
struct Lifter {
    template<Receiver R>
    Receiver lift(R);
};

Executor concepts

  1. Via (proposed to replace SemiFuture)
struct Via {
    template<Executor Ex>
    Sender via(Ex);
};
  1. ViaExecutor (proposed to replace then_execute)
struct ViaExecutor {
    template<Sender S>
    Sender via_execute(S);
};
  1. BulkExecutor
struct BulkExecutor {
    template<Sender S, class Op, class ShapeF, class StateF, class ResultS>
    Sender operator()(S, Op, ShapeF, StateF, ResultS);
};

beyond the concepts

type-erasure

Each of the concepts needs a type-erasure definition in std.

'builder' types

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;
    }};
}
#include <cstddef>
#include <type_traits>
#include <exception>
#include <utility>
#include <memory>
#include <array>
namespace mc {
//
// would be implemented as customization points
//
template<class R>
void set_done(R r) { r.done(); }
template<class R, class E>
void set_error(R r, E e) { r.error(e); }
template<class R, class V>
void set_value(R r, V v) { r.value(v); }
template<class S>
auto executor(S s) -> decltype(s.executor()) { return s.executor(); }
template<class S, class R>
auto submit(S s, R r) -> decltype(s.submit(r)) { s.submit(r); }
template<class Ex, class S>
auto via_execute(Ex ex, S s) { return ex.via_execute(s); }
template<class Via, class Ex>
auto via(Via v, Ex ex) { return v.via(ex); }
template<class Ex, class S, class Op, class ShapeF, class StateF, class ResultS>
auto bulk_execute(Ex ex, S&& s, Op&& op, ShapeF&& shpf, StateF&& stf, ResultS&& rs)
-> decltype(ex.bulk_execute(s, op, shpf, stf, rs)) {
return ex.bulk_execute(s, op, shpf, stf, rs);
}
template<class A, class S>
auto adapt(A a, S s) -> decltype(a.adapt(s)) { return a.adapt(s); }
template<class L, class R>
auto lift(L l, R r) -> decltype(l.lift(r)) { return l.lift(r); }
//
// would be implemented in terms of a property system
//
template<class T>
constexpr bool is_silent_v = true;
template<class T>
constexpr bool is_none_v = true;
template<class T>
constexpr bool is_single_v = true;
template<class T>
constexpr bool is_receiver_v = true;
template<class T>
constexpr bool is_sender_v = true;
//
// basic concepts
//
template <class T>
constexpr bool implicitly_convertible_to(T) {
return true;
}
template <class T>
concept bool Object =
requires (T* p) {
*p;
implicitly_convertible_to<const volatile void*>(p);
};
template <class T, class... Args>
concept bool Constructible = std::is_constructible<T, Args...>::value;
template <class T>
concept bool MoveConstructible = Constructible<T, T>;
template <class From, class To>
concept bool ConvertibleTo =
requires (From (&f)()) {
static_cast<To>(f());
} && std::is_convertible<From, To>::value;
template <class T>
concept bool SemiMovable = Object<T> && Constructible<T, T> && ConvertibleTo<T, T>;
//
// Promise concepts
//
template <class PS>
concept bool Silent =
is_silent_v<PS>;
template <class PS>
concept bool None =
Silent<PS> && is_none_v<PS>;
template <class PS>
concept bool Single =
None<PS> && is_single_v<PS>;
template <class R>
concept bool Receiver =
requires (R& r) {
::mc::set_done(r);
} &&
SemiMovable<R> &&
is_receiver_v<R>;
// NoneReceiver aka promise<void>
//
// struct NoneReceiver {
// void done();
// template<class E>
// void error(E) noexcept;
// };
template <class N, class E = std::exception_ptr>
concept bool NoneReceiver =
requires(N& n, E&& e) {
::mc::set_error(n, (E &&) e);
} &&
Receiver<N> &&
None<N> &&
SemiMovable<E>;
// SingleReceiver aka promise<T> (subsumes NoneReceiver)
//
// struct SingleReceiver {
// void done();
// template<class E>
// void error(E) noexcept;
// template<class V>
// void value(V);
// };
template <class S, class T, class E = std::exception_ptr>
concept bool SingleReceiver =
requires(S& s, T&& t) {
::mc::set_value(s, (T &&) t) ;// Semantics: called exactly once.
} &&
NoneReceiver<S, E> &&
SemiMovable<T> &&
SemiMovable<E> &&
Single<S>;
//
// Future concepts
//
template <class S>
concept bool Sender =
requires(S& s) {
::mc::executor(s);
} &&
SemiMovable<S> &&
Silent<S> &&
is_sender_v<S>;
// NoneSender aka future<void>
//
// struct NoneSender {
// template<None R>
// void submit(R);
// };
template <class S, class R, class E = std::exception_ptr>
concept bool NoneSender =
requires(S& s, R&& r) {
::mc::submit(s, (R &&) r);
} &&
Sender<S> &&
NoneReceiver<R, E>;
// SingleSender aka future<T> and Executor (subsumes NoneSender)
//
// struct SingleSender {
// template<Single R>
// void submit(R);
// };
template <class S, class R, class T, class E = std::exception_ptr>
concept bool SingleSender =
requires(S& s, R&& r) {
::mc::submit(s, (R &&) r);
} &&
Sender<S> &&
SingleReceiver<R, T, E>;
// Adaptor - A function to chain senders
//
// struct Adaptor {
// template<Sender S>
// Sender operator()(S);
// };
template <class A, class S>
concept bool Adaptor =
requires(A& a, S&& s) {
::mc::adapt(a, (S &&) s);
} &&
Sender<S>;
// Lifter - A function to chain receivers
//
// struct Lifter {
// template<Receiver R>
// Receiver operator()(R);
// };
template <class L, class R>
concept bool Lifter =
requires(L& l, R&& r) {
::mc::lift(l, (R &&) r);
} &&
Receiver<R>;
//
// Executor concepts
//
template <class Ex>
concept bool Executor =
Sender<Ex> && Single<Ex>;
// Via (proposed to replace SemiFuture)
//
// struct Via {
// template<Executor Ex>
// Sender via(Ex);
// };
template <class S, class Ex>
concept bool Via =
requires(S& s, Ex&& ex) {
::mc::via(s, (Ex &&) ex);
requires Sender<decltype(::mc::via(s, (Ex &&) ex))>;
} &&
Executor<Ex> &&
Sender<S>;
// ViaExecutor (proposed to replace then_execute)
//
// struct ViaExecutor {
// template<Sender S>
// Sender via_execute(S);
// };
template <class Ex, class S>
concept bool ViaExecutor =
requires(Ex& ex, S&& s) {
::mc::via_execute(ex, (S &&) s);
requires Sender<decltype(::mc::via_execute(ex, (S &&) s))>;
} &&
Sender<S> &&
Executor<Ex>;
// BulkExecutor - parallelize a
// function across data via a shape that
// describes the data extents.
//
// struct BulkExecutor {
// template<Sender S, class Op, class ShapeF, class StateF, class ResultS>
// Sender operator()(S, Op, ShapeF, StateF, ResultS);
// };
template <class Ex, class S, class Op, class ShapeF, class StateF, class ResultS>
concept bool BulkExecutor =
requires(Ex& ex, S&& s, Op&& op, ShapeF&& shpf, StateF&& stf, ResultS&& rs) {
::mc::bulk_execute(ex, (S &&) s, (Op &&) op, (ShapeF &&) shpf, (StateF &&) stf, (ResultS &&) rs);
requires Sender<decltype(::mc::bulk_execute(ex, (S &&) s, (Op &&) op, (ShapeF &&) shpf, (StateF &&) stf, (ResultS &&) rs))>;
} &&
Sender<S> &&
Executor<Ex>;
} // namespace mc
namespace mc {
//
// Toy implementation for exposition
//
//
// inline Executor
//
struct my_inline_executor {
my_inline_executor executor() { return *this; }
template<Single S>
void submit(S s) {::mc::set_value(s, *this);}
};
template<Sender S>
auto via_execute(my_inline_executor me, S s);
template<Sender S, class Op, class ShapeF, class StateF, class ResultS>
void bulk_execute(my_inline_executor me, S s, Op op, ShapeF shpf, StateF stf, ResultS rs);
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>;
template<class SubmitFn>
struct my_future {
SubmitFn submitfn_;
template<class TransformFn>
auto then(TransformFn tfn);
my_inline_executor executor() { return {}; }
template<class SingleReceiver>
void submit(SingleReceiver out) {
submitfn_(std::move(out));
}
template<class T>
auto get();
};
template<class SubmitFn>
my_future(SubmitFn) -> my_future<SubmitFn>;
template<class SubmitFn>
template<class TransformFn>
auto my_future<SubmitFn>::then(TransformFn tfn) {
return ::mc::my_future{[submitfn = this->submitfn_, tfn = std::move(tfn)](auto out) mutable {
submitfn(my_promise{std::move(out), [tfn](auto out, auto v) mutable { ::mc::set_value(out, tfn(v)); }});
}};
}
template<class SubmitFn>
template<class T>
auto my_future<SubmitFn>::get() {
T result;
::mc::submit(*this, my_promise{&result, [](T* r, auto v){ *r = v; }});
return result;
}
template<class AdaptFn>
struct my_adaptor {
AdaptFn adaptfn_;
template<Sender S>
auto adapt(S in) {
return adaptfn_(std::move(in));
}
};
template<class AdaptFn>
my_adaptor(AdaptFn) -> my_adaptor<AdaptFn>;
template<class LiftFn>
struct my_lifter {
LiftFn liftfn_;
template<Receiver R>
auto lift(R out) {
return liftfn_(std::move(out));
}
};
template<class LiftFn>
my_lifter(LiftFn) -> my_lifter<LiftFn>;
template<Sender S>
requires Sender<S>
auto pipe(S in) {
return in;
}
template<Sender S, Adaptor<S> A0, class... AN>
requires Sender<S> && Adaptor<A0, S> && sizeof...(AN) > 0
auto pipe(S in, A0 a0, AN... an) -> decltype(pipe(::mc::adapt(a0, in), an...)) {
return pipe(::mc::adapt(a0, in), an...);
}
template<Sender S, Adaptor<S> A0>
requires Sender<S> && Adaptor<A0, S>
auto pipe(S in, A0 a0) {
return ::mc::adapt(a0, in);
}
template<Sender S, class L0>
requires Sender<S> && !Adaptor<L0, S>
auto pipe(S in, L0 l0) {
return my_future{[in, l0](auto out){ ::mc::submit(in, ::mc::lift(l0, out)); }};
}
template<Sender S, class L0, class L1, class... AN>
requires Sender<S> && !Adaptor<L0, S> && !Adaptor<L1, decltype(pipe(std::declval<S>(), std::declval<L0>()))>
auto pipe(S in, L0 l0, L1 l1, AN... an) {
return pipe(in, my_lifter{[l0, l1](auto out){ return ::mc::lift(l0, ::mc::lift(l1, out)); }}, an...);
}
template<Sender S, class L0, class A0, class... AN>
requires Sender<S> && !Adaptor<L0, S> && Adaptor<A0, decltype(pipe(std::declval<S>(), std::declval<L0>()))>
auto pipe(S in, L0 l0, A0 a0, AN... an) {
return pipe(pipe(in, l0), a0, an...);
}
//
// basic operators
//
template<class TransformFn>
auto transform(TransformFn tfn){
return my_lifter{[tfn = std::move(tfn)](auto out){
return my_promise{std::move(out), [tfn](auto out, auto v) mutable { ::mc::set_value(out, tfn(v));}};
}};
}
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;
}};
}
//
// Executor operators
//
template<class ContinuationFn>
auto execute(ContinuationFn cfn){
return my_adaptor{[cfn](auto ex) {
::mc::submit(ex, my_promise{cfn, [](auto cfn, auto ex) {
cfn();
}});
}};
}
template<class ContinuationFn>
auto twoway_execute(ContinuationFn cfn){
return my_adaptor{[cfn](auto ex) {
return my_future{[cfn, ex](auto out) {
::mc::submit(ex, my_promise{out, [cfn](auto out, auto ex){
::mc::set_value(out, cfn());
}});
}};
}};
}
template<Sender S>
auto via_execute(S s){
return my_adaptor{[s = std::move(s)](auto ex){
return via_execute(ex, s);
}};
}
template<Executor Ex>
auto via(Ex ex){
return my_adaptor{[ex = std::move(ex)](auto s){
return via_execute(ex, s);
}};
}
template<Sender S, class Op, class ShapeF, class StateF, class ResultS>
auto bulk_execute(S s, Op op, ShapeF shpf, StateF stf, ResultS rs){
return my_adaptor{[s = std::move(s), op, shpf, stf, rs](auto exs){
return my_future{[exs, s, op, shpf, stf, rs](auto out) mutable {
::mc::submit(exs, my_promise{out, [s, op, shpf, stf, rs](auto out, auto ex) mutable {
bulk_execute(ex, s, op, shpf, stf, rs);
::mc::set_value(out, ex);
}});
}};
}};
}
//
// via_execute customization point (proposed replacement for then_execute)
//
template<Sender S>
auto via_execute(my_inline_executor me, S s) {
return my_future{[s, ex = me](auto out) mutable {
::mc::submit(s, my_promise{out, [ex](auto out, auto v) mutable {
::mc::submit(ex, my_promise{out, [v](auto out, auto ex){::mc::set_value(out, v);}});
}});
}};
}
//
// bulk_execute customization point
//
template<Sender S, class Op, class ShapeF, class StateF, class ResultS>
void bulk_execute(my_inline_executor me, S s, Op op, ShapeF shpf, StateF stf, ResultS rs) {
::mc::submit(s, my_promise{me, [op, shpf, stf, rs](auto ex, auto v) mutable {
auto shape = shpf(v);
auto st = stf(shape, v);
for(auto i : shape) {
op(v, i, st);
}
rs(st);
}});
}
//
// alternative bulk / async task dispatch pattern
//
// async result sender
template<class Ex, class SubmitFn>
struct my_result {
Ex ex;
SubmitFn submitfn_;
Ex executor() { return ex; }
template<class SingleReceiver>
void submit(SingleReceiver out) {
submitfn_(std::move(out));
}
};
template<class Ex, class SubmitFn>
my_result(Ex, SubmitFn) -> my_result<Ex, SubmitFn>;
struct my_gpu_executor {
my_gpu_executor executor() { return *this; }
template<Single S>
void submit(S s) {::mc::set_value(s, my_result{*this, [](auto out){set_done(out);}});}
};
//customization point
template<class V, class Op, class ShapeF, class StateF, class ResultS>
auto custom_bulk_transform(my_gpu_executor ex, V v, Op op, ShapeF shpf, StateF stf, ResultS rs) {
auto shape = shpf(v);
auto st = stf(shape, v);
for(auto i : shape) {
op(i, st);
}
return my_result{ex, [rs, st](auto out){set_value(out, rs(st));}};
}
// async bulk operation
template<class Op, class ShapeF, class StateF, class ResultS>
auto bulk_transform(Op op, ShapeF shpf, StateF stf, ResultS rs){
return my_adaptor{[op, shpf, stf, rs](auto in) {
return my_future{[op, shpf, stf, rs, ex = in.executor(), in](auto out) {
submit(in, my_promise{std::move(out), [op, shpf, stf, rs, ex](auto out, auto v) mutable {
::mc::set_value(out, custom_bulk_transform(ex, v, op, shpf, stf, rs));}}
);
}};
}};
}
// async join
auto join(){
return my_lifter{[](auto out){
return my_promise{std::move(out), [](auto out, auto v) mutable {
submit(v, out);
}};
}};
}
} // namespace mc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment