executors / futures

A proposal for a futures programming model for ISO C++
22 stars 7 forks source link

`future<void>` #91

Open brycelelbach opened 6 years ago

brycelelbach commented 6 years ago

We've never really discussed void. One of the most annoying things about writing futures today is dealing with void. E.g. you end up doing:

template <typename T>
struct continuatble_future;

template <typename T>
struct continuable_future
{
   template <typename Executor>
   auto via(Executor&& exec);

   template <typename Continuation>
   continuable_future</*...*/> then(Continuation&& c);
};

template <t>
struct continuable_future<void>
{
   template <typename Executor>
   auto via(Executor&& exec);

   template <typename Continuation>
   continuable_future</*...*/> then(Continuation&& c);
};

The executors proposal does special case void as well - so perhaps the burden has moved to executor authors instead of future authors (although often they are the same person).

I'd like to discuss alternatives to void. void isn't regular, so it complicates everything - from the wording to the implementation.

RedBeard0531 commented 6 years ago

We are still on C++14 for a few more weeks so I don't know for sure, but I expect that the majority of the pain for future<void> will be solved by if constexpr(is_void_v(T)) in C++17. That could be due to a peculiarity of our implementation which holds a future<FakeVoid>, and delegates much of that weirdness to a gross set of helpers to call user callbacks.

Does anyone have much experience writing future<void> in a c++17 codebase? Is the grass any greener?

LeeHowes commented 6 years ago

@yfeldblum any opinion on this? Currently we do not use void here.

yfeldblum commented 6 years ago

It depends on whether we need futures to represent results of computations, which can be void in C++, rather than representing values, which cannot be void. There is a tradeoff here. Without void, implementations are simpler, but futures cannot faithfully represent results of computations.

It seems like it would be useful for futures and coroutines to play well together, and coroutines may return void. Coroutine promises and futures must support that.

In terms of additional complexity, I am more concerned about forcing it onto authors of generic combinators and combinator libraries (e.g., on_value) than I am about forcing it onto authors of future implementations. Perhaps if constexpr can lessen that concern, but it will not eliminate it - they will still have to remember to do the conditional each time.

So I am mixed.

RedBeard0531 commented 6 years ago

Could it be conditionally supported along the lines of "If the Future implementations supports Future, the signatures and behavior shall be ..."?

On particular case where we found future<void> to come up is as the only correct return type for Socket::sendMessage(Message) where message knows its size and sendMessage either sends the whole message or fails with an error. This doesn't feel like a "computation" in all but the loosest sense.

yfeldblum commented 6 years ago

(As I was using the term, it's an effectful/side-effecting computation, as v.s. a pure/side-effect-free computation.)

griwes commented 6 years ago

You can implement support for void by annoyingly wrapping every use of T into something like std::conditional<std::is_void_v<T>, fakevoid, T> and then doing silly things in user-facing places to actually support the different signatures. I think this is the best strategy, and one reason for that is it's going to hurt less than a specialization when (if?) regular void lands.

I'm 100% okay with people having futures that don't (currently) support void. As @yfeldblum mentioned - if your computation is free of side effects, and you know that your future type is only for those computations, then going out of your way to support void would be nonsensical. So I'd say that we don't require futures to always support void, and we just add weasel wording for the concrete types to say what we mean just like #105 does.

brycelelbach commented 6 years ago

In an earlier version of HPX, we had a struct unused_type { }; that we used to ease implementation; in prototypes that @griwes and I have mocked up, I've used a similar rvoid struct (e.g. regular void).

brycelelbach commented 6 years ago

Could it be conditionally supported along the lines of "If the Future implementations supports Future, the signatures and behavior shall be ..."?

Yes, that would be one solution.

I think @yfeldblum's point is a strong one - this won't just be a burden for future authors, but also for those writing generic code that consumes futures.

That said, it will also be a burden to users if void isn't returned, as they'll have to create some helper empty struct to return from their continuations.

brycelelbach commented 6 years ago

We should discuss this on a call.

yfeldblum commented 6 years ago

Note that we already have std::monostate as a unit type.

griwes commented 6 years ago

I was very careful not to use a publicly-usable type, becuase users will end up with a future<std::monostate>, for whatever reason.

RedBeard0531 commented 6 years ago

FWIW, I just conducted an informal poll of developers around me. Nobody wants to just a futures lib that requires explicit use of std::monostate or anything like that.

return get_an_int().then([](int i) {
    use(i);
    return std::monostate();
}).then([](std::monostate) {
    doNextThing();
    return std::monostate();
});

seems much grosser than:

return get_an_int().then([](int i) {
    use(i);
}).then([] {
    return doNextThing();
});

Or in that simple case, removing the wrapping lambdas to just call the existing functions (if they exist).

If a Future implementer wants to use monostate as in internal implementation detail, that's fine. But it seems bad to let that detail leak out to the public API.

griwes commented 6 years ago

My point is that monostate internally is a bad idea, because it turns future<std::monostate> functionally into future<void>, and that's incredibly bad.

We should just have an internal, private type for this.

LeeHowes commented 6 years ago

We don't want to force people to use whatever the type is, though.

So I wonder what are the generic use cases?

Do we want to be able to write a like this:

auto f(auto&& aFuture) {
   return std::move(sFuture.then(on_value([](auto val){return val;}));
}

in which case we'd just want a void future to be able to accept a void function. Or, actually, for the on_value function to be able to accept it cleanly, which is not the same thing. The wrapper that on_value constructs could just abstract this:

auto on_value(F&& func) {
    struct {
      operator()() {
        return func(void_type);
      }
     operator()(auto arg) {
       return func(std::move(arg));
      }
    }:
}

and we benefit from the power of not tying this to the future at all. A passthrough function would mean dealing with the return type too but the future need not know either way.

If, on the other hand, we always want to be able to take a void lambda, then we can do the reverse of dropping the argument in the wrapper function. Again the future need not know about it.

LeeHowes commented 6 years ago

Obviously ignore the obvious mistakes in the above, like not returning the struct, not having return types, misusing move... you get the picture though.

dhollman commented 6 years ago

So does this basically come down to whether we want to have a specialization of the concept FutureContinuation<void> or a specialization of the concept ContinuableFuture<void>? I think I can see either or both being reasonable, but I think the point of this issue is that we can't do neither of the two.

In the former case, on_value() would have to detect when it's been given a nullary function (remember, on_value() doesn't know about T) and return a FutureContinuation<void>, and ContinuableFuture<T>::then() would just be constrained to take a FutureContinuation<T>() for all T (though I guess the invocation semantics of the actual type, when implemented, would need to be specialized).

In the latter case, I think you end up having to specialize both, though I guess you could re-specify FutureContinuation<T> to take a wrapped<T> or something and do the specialization there (related to what @griwes was suggesting, I think), but that seems pretty ugly as a thing to expose at the specification level.

griwes commented 6 years ago

Couild we solve the continuation problem by providing on_void? (Or is that too generic-code-breaking? I didn't think much about this aspect.)

dhollman commented 6 years ago

Couild we solve the continuation problem by providing on_void?

Yes, but not the specification problem. Could be a good idea, though.

LeeHowes commented 6 years ago

Yes we should support Future for all the Future concepts and standard types.

Yes it just calls FutureContinuation::value_completion_function(no parameter goes here) when it is void

The clever thing about the helper functions is that they can handle this. If we really want it to be completely generic we can have an on_generic_value that constructs (and deconstructs) a magic thing and converts an incoming void to it.