Asynchronous UI code (was: Re: Proposal to improve Promises (was Re: Re: The Inbox: Kernel-tonyg.1610.mcz))
Christoph Thiede via Squeak-dev <[email protected]>
| Newsgroups | gmane.comp.lang.smalltalk.squeak.general |
|---|---|
| Message-ID | <de61f598-dbe3-4d64-8878-d8496c726163@MX2018-DAG1.hpi.uni-potsdam.de> |
Hi Jaromir, Lauren,
> Time flies and the code is years old by now, so I would have to dig
> deep into it or think hard to make any promises (pun intended) which
> way would work best, but I do not want to spend my free time with that
> right now. (I seem to rather spend it writing such emails at midnight,
> sigh.)
I was surprised and appreciate that you still seem to be reading (and replying to!) the list on a daily basis. Good to hear from you!
> As you can see in the code that you, Christoph, quoted, the purpose of
> most handlings of BrokenPromise is to react to the user cancelling the
> interaction, and to cancel the overall workflow, too. I vaguely recall
> that the promise (of completing the user interaction) gets rejected
> with #cancelled if the user cancels, but I am not sure. Thus this is
> not really an error/exception situation. So maybe one way would be to
> change the implementation to not reject the promises in such cases but
> resolve them with a special cancel value and check for that. That
> sounds like making it as ugly as null checks, but maybe that is not
> less pretty than the current code handling BrokenPromise.
Good point! I guess a more idiomatic (and promise-invariant) way to express this in Smalltalk would be an extra Abort exception that either rejects the current promise or may cancel the current UI operation if unhandled. Still, thinking about this makes we wonder again whether we really want to hide the fact that an error came from the original resolver of a promise and not from code on the current process/stack. It feels wrong to me ... To much surprise ...
--- (below is not relevant for the release/backward compatibilty discussion but still very interesting) ---
(By the way, I also find it a bit confusing that terminating the fulfiller block of a promise does not reject the promise. Shouldn't we use ifCurtailed: here?
Promise new
in: [:p | [p fulfillWith: [Processor terminateActive] passErrors: false] fork];
value)
> What I can give you as context is why I used promises there. I sought
> for something that allows me to code multi-step workflows that are
> interrupted with user interactions (i. e. compute something, then ask
> something from the user via a dialog, then continue, maybe another
> interaction with waiting, continue, ...) with the following criteria:
> - not obscuring the overall workflow too much by tearing it apart
> across many methods that don't seem to invoke each other directly,
> - being able to use the Restart button in the debugger to easily
> roll back part of the workflow without having to restart from the very
> beginning and repeating lots of clicks,
> - related to the above: not losing access to the contexts of what
> happened before the last user interaction ("How did I get here?!"),
> - keeping the workflow implementation somewhat decoupled from the
> GUI implementation, especially the wiring what happens after you click
> Ok or Cancel,
> - without modal dialogs because I strongly dislike how they break in
> Monticello (or used to, I don't know whether anything has changed
> there in the meantime).
This problem sounds very familiar to me. The current approaches we are having for this are as follows (if anyone else knows another way, please let me know!):
- World subcycles that are run in a loop until a dialog has been completed. This is currently used by DialogWindows (e.g., self inform: '...'; see DialogWindow>>#getUserResponse) and, in a less elegant fashion, by the ToolBuilder (e.g., for Monticello dialogs and even the SquotChangesChooser; see MorphicToolBuilder>>#runModal:):
MorphicToolBuilder>>runModal: aWidget
"Run the (previously opened) widget modally, e.g.,
do not return control to the sender before the user has responded."
[aWidget world notNil] whileTrue: [
aWidget outermostWorldMorph doOneCycle.
].
(getUserResponse is similar but less compact cuz more careful)
By the way, this models the behavior of MVC quite closely, which uses these "nested cycles" or "small loops" by default for processing events in the current view (see Controller>>#startUp), but the difference is that MVC by design is not truely multitasking-capable for handling events in different views (thinking about this: what are the actual limitations of their approach?).
- Promises (clumsy because it requires manual awaits and we don't even have native await support in the trunk and they do not support resumable exceptions etc).
- Tweak (without ever having used it, presumably clumsy to some extent as well because like promises it requires to explicitly acknowledge this concern in your code).
- UI process suspension & resumption. This one is quite intersting. Used by the Debugger when you break or interrupt the UI process (e.g., self halt) and proceed later, or also by the SyntaxError window (e.g., Compiler evaluate: '+'). When the debugger is opened on the active UI process, that process is suspended and a new UI process is started by the project instead. When the debugger is abandoned, we simply keep the new UI process running. When the debugger is proceeded, we stop the new UI process and resume the previous one. See Project>>#debugProcess:inWindow: and Project>>#resumeProcessSafely:.
In fact, I tend to like this one most. Yes, it breaks world subcycles (this is why dialog boxes disappear or Monticello dialogs say "was interrupted" after you stopped an erroneous do-it), but it is more robust than world subcycles. Both subcycles and UI process S&R effectively model a stack of pending interactions, but a single Cmd + Dot and Abandon can delete an entire stack of world subcycles, whereas closing a debugger only pops one element from the suspended process stack.
I'm seriously considering whether we should migrate dialog windows of all kinds to the S&R approach at some point. This seems the closest we can approach the "small loops" style of MVC without losing the centralized control of Morphic. And in comparison to promises/tweak/continuations, it is fully transparent in the application code, retaining the current call stack and exception handlers and not requiring use of additional metaprogramming language.
Okay, enough off-topic. Back to work...! :D But this is an interesting matter. :-) Keep your ideas coming!
Best,
Christoph
--
Sent from Squeak Inbox Talk
On 2026-04-29T19:39:56+02:00, [email protected] wrote:
> Hi Lauren,
>
> I like Common Lisps and conditions&restarts but likewise did not embark on an implementation in Smalltalk.
>
> The user interactions are more than simple choices of direction. Examples: selecting which changes in the working copy to commit and which not; which to load; handling a merge; if a push or fetch failed due to authentication issues, asking the user for new credentials.
>
> If restarts can accommodate such "bigger" prompts and results, they might be a nice solution. But alas not available in standard Squeak, so not a solution in reach even if implemented somewhere else.
>
> Kind regards,
> Jakob
>
> Lauren P <[email protected]> schrieb am Mi., 29. Apr. 2026, 11:01:
> Hi Jakob,
>
> Is this far off the idea?
>
> It sounds like you want continuations, but at specific places you ask the user to proceed to the! next step or do something else (including abandoning). There might also be context-sensitive branches you can choose from based on which part of the continuation you're in when the computation stops (due to error or interrupt).
>
> If so, that is a Common Lisp restart. I was playing with implementing them, but never needed one enough to warrant spending much time on it.
>
>
> On Tue, Apr 28, 2026, 16:29 Jakob Reschke <[email protected]> wrote:
> Time flies and the code is years old by now, so I would have to dig
> deep into it or think hard to make any promises (pun intended) which
> way would work best, but I do not want to spend my free time with that
> right now. (I seem to rather spend it writing such emails at midnight,
> sigh.)
>
> What I can give you as context is why I used promises there. I sought
> for something that allows me to code multi-step workflows that are
> interrupted with user interactions (i. e. compute something, then ask
> something from the user via a dialog, then continue, maybe another
> interaction with waiting, continue, ...) with the following criteria:
> - not obscuring the overall workflow too much by tearing it apart
> across many methods that don't seem to invoke each other directly,
> - being able to use the Restart button in the debugger to easily
> roll back part of the workflow without having to restart from the very
> beginning and repeating lots of clicks,
> - related to the above: not losing access to the contexts of what
> happened before the last user interaction ("How did I get here?!"),
> - keeping the workflow implementation somewhat decoupled from the
> GUI implementation, especially the wiring what happens after you click
> Ok or Cancel,
> - without modal dialogs because I strongly dislike how they break in
> Monticello (or used to, I don't know whether anything has changed
> there in the meantime).
>
> Promises did not really make me completely happy. I also tried other
> things, such as having the workflow in another Process. I was
> interested in continuations, too, but never fancied to roll yet
> another implementation of them or pull in another dependency for them.
>
> Which means: it is not mandatory that the Squot implementation stay
> with promises or with relying on BrokenPromise. Whoever takes time to
> look into the issue, and finds a simpler, stable alternative: feel
> free to change it. Maybe it is better without promises, or maybe it is
> simple to adapt the code to this breaking change and stick to
> promises, I don't know.
>
> As you can see in the code that you, Christoph, quoted, the purpose of
> most handlings of BrokenPromise is to react to the user cancelling the
> interaction, and to cancel the overall workflow, too. I vaguely recall
> that the promise (of completing the user interaction) gets rejected
> with #cancelled if the user cancels, but I am not sure. Thus this is
> not really an error/exception situation. So maybe one way would be to
> change the implementation to not reject the promises in such cases but
> resolve them with a special cancel value and check for that. That
> sounds like making it as ugly as null checks, but maybe that is not
> less pretty than the current code handling BrokenPromise.
>
> Also note this ifError:ifNotError: extension... I wrote that and it
> looks like at least one reason for it was a different incompatible
> change in promises around Squeak 5.3 already. (I vaguely remember
> being in favor of that change though.) How much patching will it take
> this time to make the adapted code work both in the latest Squeak and
> in slightly older Squeak versions? Thus the question in my last email
> was serious. If nobody finds a nice way to achieve that, well, then
> this is a very annoying breaking change!
>
> Kind regards,
> Jakob
>
> Am Mo., 27. Apr. 2026 um 23:04 Uhr schrieb Thiede, Christoph
> <[email protected]>:
> >
> > I mentioned you because you might understand your code base best. :D Quickly skimming through some representative references to BrokenPromise on the develop branch:
> >
> > [[[answer := aWaitable wait]
> > on: BrokenPromise do:
> > [:bp | bp ifError: [:error | exception := error. bp return] ifNotError: [bp pass]]
> > on: Error do:
> > [:e | exception := e. e return]]
> >
> > This is easy:
> >
> > [[[answer := aWaitable wait]
> > on: (Smalltalk at: #BrokenPromise) do:
> > [:bp | bp ifError: [:error | exception := error. bp return] ifNotError: [bp pass]]
> > on: Error do:
> > [:e | exception := e. e return]]
> >
> > (The second handler will now handle the failed promise.)
> >
> > self withUnitOfWork:
> > [[SquotGUI waitFor:
> > ((workingCopy newInteractiveSaveOperation
> > title: 'Select changes and message for the new version';
> > applyToWorkingCopy)
> > then: [:result | self refresh])]
> > on: BrokenPromise do: [:e | "cancelled" e return]].
> >
> > This is harder: You explicitly only search for asynchronous errors but not for synchronous errors. If the operation can be assumed not to raise synchronous errors as well, simply rewrite:
> >
> > self withUnitOfWork:
> > [[SquotGUI waitFor:
> > ((workingCopy newInteractiveSaveOperation
> > title: 'Select changes and message for the new version';
> > applyToWorkingCopy)
> > then: [:result | self refresh])]
> > on: Error do: [:e | "cancelled" e return]].
> >
> > If the operation might also raise synchronous errors, this is not something we could easily catch with the new refactoring. If necessary, you could test whether the signaler context of the error was Promise>>#signalErrorValue.
> >
> > [SquotGUI waitFor: promise] "will be resolved with a SquitAddRemote or rejected with #cancelled"
> > on: BrokenPromise do: [:e | e ifNotError: [^ self undo]].
> >
> > No idea when non-erroring broken promises are raised. Only in Squeak 5.3 and older? Then simply:
> >
> > [SquotGUI waitFor: promise] "will be resolved with a SquitAddRemote or rejected with #cancelled"
> > on: (Smalltalk at: #BrokenPromise) do: [:e | e ifNotError: [^ self undo]].
> >
> > Overall, I think this should be doable. But I also mentioned you because I would be interested in your opinion on this refactoring - does it prevent possible use cases you would have in mind or does it only streamline working with promises and prevent bad ideas?
> >
> > Best,
> > Christoph
> > ________________________________
> > Von: Jakob Reschke <[email protected]>
> > Gesendet: Montag, 27. April 2026 21:31 Uhr
> > An: The general-purpose Squeak developers list <[email protected]>
> > Cc: Thiede, Christoph <[email protected]>
> > Betreff: Re: [squeak-dev] Re: Proposal to improve Promises (was Re: Re: The Inbox: Kernel-tonyg.1610.mcz)
> >
> > If somebody volunteers to refactor Squot to work without
> > BrokenPromise, will the result also work on older Squeak releases?
> >
> >
> > Am Mo., 27. Apr. 2026 um 19:54 Uhr schrieb Christoph Thiede via
> > Squeak-dev <[email protected]>:
> > >
> > > Hi Jakob, Tony, Chris, all,
> > >
> > > I only found out today that BrokenPromise has been deleted from the trunk. This is quite a breaking change.
> > >
> > > For example, one can no longer install Squot (which is recommended in the Preference Wizard) into a recent trunk image because it handles and extends BrokenPromises in multiple places.
> > >
> > > Regarding functionality, it is no longer possible to (i) disambiguate an exception raised asynchronously from a regular exception, (ii) access the original signaler context of the inner exception, or (iii) identify the causing promise. Squot seems to only do (i), while I think (ii) and maybe (iii) would be helpful debugging features (when I occassionally witnessed sporadic bugs in Squot in a few years ago, it was already hard enough to find out where they came from because the stack was not preserved, and not even preserving the signaler context or the resolvers will likely worsen such situations).
> > >
> > > That being said, I also understand that this extra exception wrappers felt opaque and cumbersome. Evolving APIs vs retaining backward-compatibility always is a tradeoff. Not sure what's more important.
> > >
> > > At the very least, why do we have to raise a generic Error rather than a specific one if errorValue is not an exception?
> > > For the other case, maybe we would need a way to attach multiple tags to exceptions to preserve information such as original signaler contexts or re-signaling promises to the original exception object without removing other information. Like, Exception>>tagDictionary instead of Exception>>tag? Maybe a transparent wrapper around the original exception class that forwards all unknown requests to the original exception (though this would not preserve identity, if that matters)? Maybe something else?
> > >
> > > For the upcoming release, I suggest we should do at least one of the following options:
> > >
> > > (i) Update Squot to no longer depend on BrokenPromise (it is not actively being maintained right now and a successor is in development, but still our recommendation in the preference wizard)
> > > (ii) Provide backward compatibility for BrokenPromise (though I don't know how we could achieve that)
> > >
> > > Also, we should document this breaking change in the release notes.
> > >
> > > Best,
> > > Christoph
> > >
> > > --
> > > Sent from Squeak Inbox Talk
> > >
> > > On 2025-08-12T10:15:25+02:00, [email protected] wrote:
> > >
> > > On 8/8/25 03:24, Chris Muller wrote:
> > > > It might be fine, but please let me know: If the settlement
> > > > of one Promise utilizes a variable which refers to another, yet
> > > > unsettled Promise, and that inner promise is rejected, how should the
> > > > (outer) Promise respond? I guess it would be Error Error Error up the
> > > > Promise chain; likely each signaled from different Processes, is that
> > > > right? Or, do we only want one signal from the "root" Promise?
> > >
> > > Promises do chain together, yes. The implementation in Squeak already
> > > does something reasonably sensible (I'm sure there are more corner cases
> > > waiting to be discovered), drawing heavily on https://promisesaplus.com/.
> > >
> > > > But yes, for how *I* use Promises, this proposal is absolutely fine.
> > >
> > > Thanks. I'll go ahead and merge then, I think! I have a small queue of
> > > improvements to Promises that will follow.
> > >
> > > Best,
> > > Tony
> > >
> > > Squeak-dev mailing list -- [email protected]
> > > To unsubscribe send an email to [email protected]
> Squeak-dev mailing list -- [email protected]
> To unsubscribe send an email to [email protected]
>
> Squeak-dev mailing list -- [email protected]
> To unsubscribe send an email to [email protected]
Squeak-dev mailing list -- [email protected]
To unsubscribe send an email to [email protected]