Promises in D

Vladimir Panteleev thecybershadow.lists at gmail.com
Thu Apr 8 11:55:37 UTC 2021


On Thursday, 8 April 2021 at 11:13:55 UTC, Sebastiaan Koppe wrote:
> On Thursday, 8 April 2021 at 09:31:53 UTC, Vladimir Panteleev 
> wrote:
>> I see, thanks! So, if I understand correctly - to put it in 
>> layman terms, senders/receivers is just a structured way to 
>> chain together callables, plus propagating errors (as with 
>> promises), plus cancellation. I understand that `setValue` 
>> just calls the next continuation with its argument (as opposed 
>> to storing the value somewhere as its name might imply), which 
>> means that the value may reside on the stack of the sender's 
>> start function, and remain valid only until `setValue` exits.
>> The API is also somewhat similar, and I understand the main 
>> distinction is that starting execution is explicit (so, and 
>> the end of your `.then` chain, there must be a `.start()` call 
>> OSLT).
>
> Yes, but be aware that the callee of .start() has the 
> obligation to keep the operational state alive until *after* 
> one of the three receiver's functions are called.

Sorry, what does operational state mean here? Does that refer to 
the root sender object (which is saved on the stack and 
referenced by the objects implementing the intermediate 
steps/operations)? Or something else (locals referred by the 
lambdas performing the asynchronous operations, though I guess in 
that case DMD would create a closure)?

Also, does this mean that this approach is not feasible for 
`@safe` D?

> Often, instead of calling `.start` you would call `.sync_wait`, 
> or just return the sender itself (and have the parent take care 
> of it).

I'm finding it a bit difficult to imagine how that would look 
like on a larger scale. Would it be possible to write e.g. an 
entire web app where all functions accept and return senders, 
with only the top-level function calling `.start`?

Or is there perhaps a small demo app making use of this as a 
demonstration? :)

>> I see how you could write a fiber-based executor/scheduler, 
>> but, I don't see how you could use these as a base for a 
>> synchronous fiber API like async/await. With delegates (and 
>> senders/receivers), there is a known finite lifetime of the 
>> value being propagated. With async/await, the value is 
>> obtained as the return value of `await`, which does not really 
>> provide a way to notify the value's source of when it is no 
>> longer needed.
>
> Hmm, I see. But isn't that the limitation of async/await 
> itself? I suppose the solution would be to build refcounts on 
> top of the value, such that the promise hold a reference to the 
> value (slot), as well as any un-called continuations. Which 
> would tie the lifetime of the value to that of the promise and 
> all its continuations.

Logically, at any point in time, a promise either has un-called 
continuations, OR holds a value. As soon as it is fulfilled, it 
schedules all registered continuations to be called as soon as 
possible. (In reality there is a small window of time as the 
scheduler runs these continuations before they consume the value.)

We *could* avoid having to do reference counting or such with 
promises if we were to:

1. Move the value into the promise, thus making the promise the 
value's owner

2. Call continuations actually immediately (not "soon" as 
JavaScript promises do)

3. Define that continuation functions may only use the value 
until they return.

With these modifications, it is sufficient to make the promise 
itself reference-counted (or, well, non-copyable). When it is no 
longer referenced / goes out of scope, all consumers of the value 
will have been called, and no more can be registered.

However, these modifications unfortunately do make such promises 
unusable for async/await. Here, the continuation is the fragment 
of the `async` function from that `await` and only until the next 
`await` (or the return). We can't really make any assumptions 
about the lifetime of the value in this case. (I think the same 
applies to fibers, too?)

The "call soon" requirement is interesting because it does help 
avoid an entire class of bugs, where something N levels deep 
removes the rug from under something N-10 levels deep, so I guess 
it's a trade-off between performance and potential correctness.

> Ultimately this is all caused by the promise's design. 
> Specifically the fact that you can `.then` the same promise 
> twice and get the same value. Senders/Receivers don't have 
> this. You get the value/error/done exactly once. Calling start 
> again is not allowed.

Yeah, I see. They don't hold a copy of the value at all, but are 
just a protocol for passing them around to the next processing 
step.



More information about the Digitalmars-d mailing list