Thanks so much for this discussion! I am deep in my other projects, at this time. I will definitely add this test to my implementation of Promises. I am not handling them very well. I regressed. I used to be able to Halt inside an eventual send stack, but not at this time. I believe my Halts are silently consumed. Not a good situation. It is on my list. More in a few...

• rabbit ❤️‍🔥🐰

On 8/23/23 17:20, Jakob Reschke wrote:
Hi Christoph, and everyone else who is still interested,

Who would have thought: it is not so easy.

The existing test cases already look quite as desired: future sends are expected to signal errors. But only if there is no rejection callback attached to their promise! Unfortunately, then:ifRejected: does register a rejection callback to reject the newly derived promise that gets returned from then:ifRejected:, even if there is eventually no rejection callback on that derived promise. For added fun, attaching callbacks to a future promise suffers from the race condition mentioned in my last message if the future send is not registered from within the UI process.

You can attach multiple rejection callbacks to a promise. Let's say there are two of them, and they both signal an exception, and both do not get caught by further callbacks. Then we should actually get two uncaught errors, so maybe two debuggers. But if we just iterate over the handlers one after another, as currently implemented, and encounter a non-resumable exception in the first callback, then the second callback would never be run, which violates the contract. Strange situation: we don't want to handle the errors, so that the debuggers can show up, but we must handle them to ensure that further callbacks are not impeded.

We could schedule all callback blocks to run asynchronously, as required in ECMAScript anyway. It might have repercussions on existing applications though.

If we make the callbacks asynchronous by using future sends, they will actually all run in the UI process. There goes the multi-process concurrency out of the window, and the GUI responsiveness if you have longer callbacks. If we fork one process per callback instead, it has a chance to break existing applications that use promises only from the UI process, or generally code that is not thread-safe.

Finally #wait enters the game. It uses semaphores to wait until another process resolves the promise. Guess what happens to the test cases if I schedule all callbacks using futureDo:at:args:?* All waits time out. The test cases run from the UI process, and so do the deferred UI messages to run the callbacks, so the UI process (test case) waits for itself (deferred message).

I am trying a heuristic to use futureDo:at:args: to schedule the callbacks if the current process is the UI process, and use fork otherwise. I don't like the heuristic because I don't fully trust it yet, and of course it couples the Promise implementation more closely to Morphic. Overall, more of the Promise test cases will become dependent on Morphic to do one or more world cycles...

Kind regards,
Jakob

*I tried using just #future at first, but got endlessly spinning loops via the deferred messages queue, generating one more promise to resolve on every next step. At least it didn't freeze the GUI like that, just made it slow.



Am So., 20. Aug. 2023 um 23:50 Uhr schrieb Jakob Reschke <jakres+squeak@gmail.com>:
We also have at least two more differences between Squeak promises and ECMAScript promises at the moment:

Promises/A+ demands that the onFulfilled and onRejected callbacks are called with a "clean" stack. [1] First, the Squeak implementation does not do that. It evaluates the callback blocks immediately when the promise changes state, and thus in the exception environment of whatever caused that. Second, this implies that a Promises/A+-conforming application cannot rely on try..catch/on:do: exception handlers anyway (not around the send of #then:, but also not around the send of #resolveWith:). Thus, it would be somewhat futile to expect exceptions to be signalled out of the promise callback blocks. The repeated stack unwinding, or ubiquitous asynchrony, also means that promises as in Promises/A+ are fundamentally not compatible with the "error -> edit any method on stack -> restart" debugging style that we are used to in Smalltalk. At least not without a debugger that is specialized in promises.

ECMAScript specifies that if no onRejected callback is provided to .then(), or rather if the onRejected argument is not callable, then rejecting the promise will throw the reason for the rejection. In Squeak terms: `p then: [:v | ...].  "==="  p then: [:v | ...] ifRejected: [:e | e signal]`. In ECMAScript 6 this is specified almost straightforward [2], whereas in the current standard it seems to be more complicated, but with the same end result of throwing the error. In practice, an uncaught exception in an ECMAScript promise chain may end up in the console. The Squeak equivalent would be to show a debugger (with an unrevealing stack trace if we would take the asynchrony requirement seriously) or to pop up a Transcript message. But in Squeak there is no default rejected handler that signals the error, so currently just nothing happens. Well, the promise gets rejected, but there is no visible reaction to an unhandled error.

Taking that into account, we could remove the ifRejected: block from the previous example and make further expectations:

    promise := (1 future / 0) then: [:result | result + 3]. "<= inspect it
    => Actual: rejected with ZeroDivide.
    Preferable: show debugger on ZeroDivide.
    Expected on abandon: announce that the promise was rejected with an uncaught error.
    Expected on proceed: new debugger "ZeroDivide does not understand +".
    Expected on abandon: announce as above, but referring to the MNU"

The "announcement" could be yet another debugger, the Transcript, or something customizable. The debugger might not always be the right choice, let's have another example:

    p := [Promise new rejectWith: 0] future value then: [:result | result + 3].
    "...send some further messages..."
    p whenRejected: [].

The current ECMAScript standard made me aware of this race condition [3]. If the example above is not run in the UI process, p might be rejected before the empty handler gets attached or afterwards. If it is rejected afterwards, a debugger must not appear. If it is rejected before, we might show a debugger, only to regret it a few microseconds later.




Am So., 20. Aug. 2023 um 23:44 Uhr schrieb Jakob Reschke <jakres+squeak@gmail.com>:
Hi Christoph,

It takes me quite a while to get back into this, and also to validate my message from three years ago. Here are my current conclusions:

I must distinguish better between the use of #future and promise chaining with #then:ifRejected:. While my #halt-s in then:ifRejected: might be nice at development time, they do not make sense in general, where ifRejected blocks are actually the error handlers, much like on:do: handlers. When I complained about the fact that promises swallow errors, it is inconsistent on my behalf to not complain about the fact that on:do: handlers can swallow errors, too... On the other hand, we could and probably should adapt future sends—everything that comes between #future and a #then: or #wait.

So I must rectify something from three years ago:

Am So., 21. Juni 2020 um 22:57 Uhr schrieb Jakob Reschke <forums.jakob@resfarm.de>:

Promises with whenRejected:/ifRejected: callbacks would no longer
swallow errors, and would only be rejected when the user aborts in the
debuggers, or if the future execution catches errors by itself and
converts them to rejected promises, so the future promise will also be
rejected. This could pose a compatibility problem for existing code.

promise := (1 future / 0) then: [:result | result + 3] ifRejected:
[:reason | #cancelled]. "<= inspect it => Actual: resolved with
#cancelled immediately. Expected with my proposed changes: it would
first show the ZeroDivide debugger, which you can abandon to resolve
with #cancelled, or proceed to a MessageNotUnderstood +. If you
abandon the MNU, the promise would be rejected with the MNU, not
#cancelled, in accordance with the Promises/A+ spec."


Promises with when/ifRejected blocks must "swallow" errors that happened in other whenResolved or whenRejected blocks because that is how this model works.

But nevertheless we should show the ZeroDivide in a debugger. The continuation to evaluate "1 / 0" is not inside of a promise callback block, hence the rules to turn the error into a rejection do not apply. Appropriately, the evaluation happens in #fulfillWith:passErrors: rather than in the blocks of #then:ifRejected: in the Promise implementation, so different on:do: handlers apply.

The example is mostly still alright, except for a mistake at the end:

    promise := (1 future / 0) then: [:result | result + 3]
       ifRejected: [:reason | #cancelled]. "<= inspect it
    => Actual: resolved with #cancelled immediately.
    Preferable: show debugger on ZeroDivide.
    Expected on abandon: resolve with #cancelled.
    Expected on proceed: new debugger "ZeroDivide does not understand +".
    Expected on abandon: resolve with #cancelled.
    Expected on proceed: the usual MNU loop..."

The inner promise that is the result of `(1 future / 0)` is actually rejected on abandon, but the `ifRejected: [:reason | #cancelled]` always gets the outer promise resolved.

Kind regards,
Jakob


Am Sa., 19. Aug. 2023 um 14:40 Uhr schrieb <christoph.thiede@student.hpi.uni-potsdam.de>:
Hi Jakob,

alright, so would you like to upload a patch to the inbox? Should anyone else review it? :-)

Best,
Christoph

---
Sent from Squeak Inbox Talk

-- 
••• rabbit ❤️‍🔥🐰