[Cuis-dev] Rebuilding on events: Experiments in simplifying UI development in Cuis inspired by Mithril.js
Paul D. Fernhout
pdfernhout at kurtz-fernhout.com
Sun May 7 08:24:47 PDT 2023
Thanks for the reply, Hilaire. Some comments below inline.
On 5/6/23 09:42, Hilaire Fernandes via Cuis-dev wrote:> Coincidentally,
a few weeks ago, I was reading about model-less GUI, not
> any particular implementation like the ones you referred in your email,
> but about the general idea to re-construct the whole or part of the GUI
> when an event occurs.
Would be curious to read the "model-less GUI" article you mention if you
could supply a link.
I updated the subject of this email to reflect that more general idea of
rebuilding the UI in response to events (which indeed is what Mithril does).
> It is true that it is not obvious to track the source of events with the
> observer pattern: triggered in one place, hooked in another ones. The
> tools to help the developer comprehension of events is not really there.
> The fact that it is based on symbol and not object does not help. I
> guess that what we all do is to search for symbol in the source code,
> i.e. #selected.
Agreed that the use of symbols can make things harder depending on the
convenient browser menu options.
In the 1990s I made a change to VisualWorks base code (locally to a
company with ENVY) so that asking for senders of a method also found
methods that included the same symbol. That was to support a
specification-driven UI system I wrote where rather large methods
defined how to hookup UI widget dependencies for forms -- but where it
could be hard to find those methods when browsing senders of the methods
they used via symbols.
It is great that in Cuis you can select a symbol in the browser (without
the leading #) and then search on senders of it or implementors of it
with the popup Smalltalk editor menu. It did take me a while to notice
that menu option though. Whereas I had at first thought that feature was
unavailable and that I would need to do the VisualWorks kludge again
because the obvious buttons in the Browser for "senders" and
"implementors" did not do that.
> Regarding the model, Morph itself is very neutral regarding the
> necessity to have one. Pluggable morph requires one but many others
> embed it (TextParagraphMorph, TextEnty Morph). And when required, there
> are generic models (ValueHolder, ListModel, ...).
Thanks for the pointer to the non-plugable morphs.
It's true there are generic models like you mentioned. But because they
inherit from Object which defines the dependency infrastructure for the
observer/dependency pattern, it is hard to know at a glance how
decoupled those models are from observer/dependency use. For example,
the PluggableListMorph (which uses a ListModel) defines an update:
method, and also sets a model: (which establishes a dependency). So it
is not clear to me how much in practice those more generic models are
being used outside of the observer/dependency pattern in Cuis. But that
may just be showing my current ignorance of Cuis details.
I made a third version of the Proof of Concept that included a
subcomponent to edit the Fahrenheight temperature that is a
TextModelMorph --- but that is in the PluggableMorph hierarchy.
I ended up having a method triggered by a button press event pull the
value out of the edit rather than having the edit put changed text into
an instance variable as the editor makes changes. That method also need
to call "self redrawNeeded" because the mouse click that triggers that
button press by itself does not trigger the redraw.
No doubt I could add some more wrapper code or such hopefully to make
the TextModelMorph push changes somewhere though. But even then I will
probably need to make additional changes to trigger the redraw.
I'm starting to think that ultimately, were this idea to move forward to
its ultimate extent, it will end up involving grabbing bits and pieces
from existing Cuis classes to build essentially a parallel class
hierarchy that does not use dependencies (and which runs while the
existing Cuis development tools also run). The goal would be to reach a
point where stuff like the actionMap instance variable and the change
method can be removed from Object and all the (parallel) development
tools in Cuis would still work. I would suggest Cuis would be "simpler"
then while still delivering the same functionality. (Others might
disagree based on the CPU use efficiency point or depending what other
kludges are needed to keep multiple morphs in sync.)
Of course, such a change to Object would break essentially all existing
Cuis applications and UI tools. But there could always be some sort of
changeset people could file in if they still wanted legacy dependency
support.
> *A few points of thinking.*
Thanks! :-)
> It is possible to hook programmatically mouse/keyboard events to a
> Morph. In your example the handlesMouseDown: and mouseButtons1xxx can be
> set programmatically, so it avoid subclassing. It may help to compose
> the GUI with sub morphs, using sub morphs improves the efficiency. In
> your example the whole rectangle of the GUI is rebuild even if only a
> small part needs to be updates.
Thanks for the suggestion to programmatically hook up mouse/keyboard
events to a Morph. Perhaps that may help with setting up a redraw after
events.
Indeed, more will likely be redrawn that strictly needed. That is a
tradeoff of CPU vs. human thought. But I am suggesting in practice it is
a good tradeoff as it reduces code complexity the programmer needs to
think about.
And I might also suggest that so much redrawing goes on so often that it
might not be that significant of an increase, especially with some minor
optimizations mentioned below.
But, this tradeoff ideally should be quantified. Personally I am less
interested in doing that quantification myself than in just being able
to write UIs that are simpler to implement and maintain. But
academically, quantification of before and after for CPU use and memory
use and lines of code expended and so on might make an interesting project.
> I am wondering if the simplicity you get from just rebuilding GUI comes
> with a hidden cost.
I agree that, as Frederick Brooks wrote about in "No Silver Bullet --
Essence and Accident in Software Engineering" that systems have a
certain level of "essential complexity" -- complexity that you can at
best push around somewhere else. And that relates to "hidden costs" you
bring up if this different approach just makes something else
significantly worse (which it might). The deep question here is whether
the observer pattern in Smalltalk may instead be "accidental complexity"
(as Brooks also mentions) for UI building that is unneeded these days?
And that is why it eventually might make sense to quantify CPU cycles
used or other measures to see how many resources if any the observer
pattern really saves over a somewhat broader redraw approach these days.
And I say "these days" because needs and implementations have changed
since the 1980s (or earlier) when this Observer pattern got started. It
might truly have been the best or only feasible choice back then. Now
that computers are 1000s of time faster, it may not matter much in most
cases. And when it does matter, those special cases can perhaps be
optimized individually.
It is also fair to debate whether code that does more computation but is
easier for a programmer to understand or change is "simpler". Rich
Hickey (mentioned in some Cuis docs) argues that "easier" things aren't
necessarily "simpler". He says easier things are just usually more
familiar and "at hand" mentally (even if they may be more complex). But
I think Rich Hickey would also say easier things can also be simpler as
long as the underlying code or processing is also simpler.
Related by Rich Hickey on "complect" versus "compose":
https://www.infoq.com/presentations/Simple-Made-Easy/
https://github.com/matthiasn/talk-transcripts/blob/master/Hickey_Rich/SimpleMadeEasy-mostly-text.md
"So "complect" actually means to braid together. And "compose" means to
place together. And we know that. Everybody keeps telling us. What we
want to do is make composable systems. We just want to place things
together, which is great, and I think there is no disagreement.
Composing simple components, simple in that same respect, is the way we
write robust software."
As a key question, is the redraw-on-events approach that Mithril uses
more composable for UI development than the (complected?)
observer/dependency pattern that Cuis uses?
In this case, I think redrawing entirely when a click (or any event
listened for) happens would meet that "simpler" definition by being not
"complected" between model and UI, which are essentially "composed"
side-by-side. Whereas an observer pattern with dependencies and changed
messages is "complected" because it tightly links ("braids together")
references to multiple objects pointing to each other and involves a lot
more message traffic (usually bidirectionally) between them (compared to
than a simple request by drawing code for a current value of a model).
But perhaps people might argue that, since the UI ultimately still
depends on the model in the drawing code, just without the push approach
of change messages. So there is a dependency between the drawing code
and the model. It is just an implicit dependency defined by how the
drawing code works -- instead of, say, explicit dependency defined by
setting up responses to changed messages in a separate initialization
method somewhere.
But because the implicit dependencies in this event-driven-rebuilding
approach are also defined in the same method that defines the UI, I
would argue, from a cognitive burden perspective, it is less cognitive
overhead to reason about the behavior of one draw method than to reason
simultaneously about both a draw (or update) method an an initialization
method which are "complected". And if you refactor larger methods that
draw/build the UI into smaller methods, I would still suggest those
smaller methods each defining part of the UI are composed side-by-side
more than "complected".
But maybe at this point I am just so used to the newer way of defining
UIs offered by Mithril so it seems simpler but is not? Especially when
considering the drawbacks -- in a Cuis environment -- of the challenge
of knowing what windows need to be redrawn when instance variables
change. As outlined above with a list below of options for dealing with
that, they all introduce a bit more complexity to what a user does with
in UI actions or in coding.
And yes, redrawing more than is strictly needed reduces one sort of
efficiency as a "hidden cost".
As I mentioned in my original post, there is a challenge for using this
idea compared to Mithril/JavaScript because there is an assumption in
Mithril that you only need to refresh one browser page at most when an
event happens, not every browser page. But in Cuis, there is no obvious
similar distinction between Cuis windows (unless one is imposed).
This is probably not a big deal for rerendering every Cuis window when a
mouse button is clicked (assuming typical windows that redraw nearly
instantaneously). A mouse click presumably happens at most once every
few seconds. The same is true for a redraw initiated by a typical timer
or typical network event that also might fire every few seconds.
But it is a much bigger CPU load if a morph (or window) responds to key
presses which might happen a few times a second. Even worse is if a
morph responds to mouse move events or mouse scroll events as there
might be dozens of those a second.
As many games demonstrate, modern CPUs (with GPU support) can indeed
often keep up with 60 to 120 frames per second of rendering. But that
takes a lot of power and computers tend to run hot then. So it would be
better to avoid having to redraw everything so frequently.
Here is a typical example of the need to redraw multiple windows
frequently. Let's say you make a morph that draws something inside its
bounds under the x and y coordinates of the mouse as the user moves the
mouse over the morph. The morph stores those mouseX and mouseY values in
instance variables when a mouse move event happens and used that
information in its drawing method (triggered from the event). So, this
morph is redrawing every time the mouse moves. This morph would be a
window or be in a window, with the window defining the boundary of how
much is redrawn when a morph thinks it is dirty (everything in the
window, which presumably may all be using data in closely related
models). Then you open some sort of companion inspector morph on that
other morph which displays the mouseX and mouseY values as text. The
inspector morph really should update itself whenever those mouseX and
mouseY values in the first morph change, which can happen every time the
mouse moves inside that first morph's boundaries. But how will the
second morph know to redraw itself because the first morph redrew itself
because of a mouse event only the first morph was listening for?
This same issue will arise as with the Cuis DependencyExamples with
shared data across multiple windows about a list of items and which item
is selected. But in practice that example only needs to redraw when an
item is clicked, so even if the entire display is redraw every few
seconds in response to a click event, that is probably not a big deal in
extra CPU use.
I've been thinking on this a bit, and here are a few possibilities to
keep secondary morphs in sync when they are inspecting objects/models
that other morphs are modifying:
1. Entirely ignore the problem and require the user to do "restore
display" as needed.
2. Have an update button on the secondary inspector morph.
3. Redraw the inspector morph anytime it is clicked anywhere.
4. Have the inspector morph redraw frequently using a timer.
5. As an optimization building on #4, the inspector morph could cache a
copy of the values it was inspecting and only redraw when they have
changed when a timer fires. This caching could be generalized somehow so
any morph might use this approach, like perhaps at the beginning of the
drawOn: method there could be code that caches all the values needed for
the drawing. This could get ugly if drawing required a lot of data and
adds to the complexity. But for an inspector on only a couple of values,
this approach might not be that ugly.
6. Establish a dependency between the second morph and the first morph
for redraw events. This starts to recreate the complexity of the
observer pattern and a push model of updates sent from a model to an
observer, but to a lesser extent because it only happens on a redraw.
So, there is not much for the user to think about in setting this up and
not as much to go wrong as with the regular observer pattern. This would
require redraw code not to change data that might trigger other redraws
in the first morph (to avoid "ringing"), but it is a best practice
anyway not to modify data in a drawing method (other than maybe to cache
a display-specific calculation result).
7. Have the redraw event in the first morph trigger a hierarchical
cascade of messages to all morphs visible on the screen. Morphs could
decide whether to redraw based on some additional information in those
messages like the morph initiating the redrawing. The second inspector
morph would implement a method that redraws itself if it sees such a
message with a reference to the morph it is inspecting. This may seem
like an observer pattern -- except that, unlike ActiveModel, there is no
knowledge in the first morph about what other morphs specifically are
observing it. I think it is less complex in that sense than the usual
observer/dependency pattern.
8. Essentially the same as #7, but with the extra information being an
id or other abstract reference to some multi-window concept representing
a set of morphs that all want to redraw together. Essentially, this
shared concept becomes similar to the idea of a browser page in
Mithril/JavaScript. Redraws then are restricted to only groups of
cooperating morphs that may all redraw when any morph in the group redraws.
There might be other options as well. On a practical basis, I think
option #4 (using a timer to redraw in any inspector) might be
satisfactory in most cases for inspectors (at least at first). Using
caching of inspected values as in #5 could then be used in the rare
cases there was any performance issues. So, this approach pushes a bit
of the complexity into inspectors. But since morphs already have support
for handling timer events, I don't see the #4 option as being that bad.
But ultimately something like #8 might make the most sense, where
windows of morphs somehow agree they are in the same group and they will
all redraw together. And where other windows not in that group don't
redraw as an optimization.
With #8 implemented this way, an inspector on a morph would somehow have
to add itself to the window group though. And arguably this introduces a
new form of observer pattern, even if it is less specific than what is
now used. But establishing this dependency between windows is an example
of the "hidden cost" you suggested -- even if arguably it is a lesser
cost in cognitive burden than creating a Cuis UI now. It has been said
that that"efficiency is where elegance goes to die" -- and the question
is how much that applies to the rebuilding on events idea in Cuis? And I
still don't fully understand the tradeoffs there, this being an experiment.
> Concerning model, don't you need one at some point to represent the
> state of your application ? So most the time the model is not a bargain
> as it already exists. Ok may be not for your converter example.
Regarding a model, yes all these UIs require a model in the sense of
needing data stored somewhere related to state.
The issue is whether that model is just plain Smalltalk objects or
whether the data needs to be held in objects that send #changed messages
or similar every time methods are called with new data. And whether you
then need to explicitly hook up that dependency with #when:send:to: or
whether you instead can just have an implicit dependency in the draw
code that reaches out to other objects to query their current state.
In that JavaScript example above, this is the model (just a plain
JavaScript object):
const model = {
amount: "100.00",
currency: "dollars",
precision: "2",
currencyError: false,
precisionError: false
}
In the Cuis Proof of Concept Fahrenheit to Celsius converter example,
the "model" is essentially just the two instance variables temperatureF
and temperatureC. Plus, the "model" includes arguably the code for
directly changing or transforming those values, which conceptually could
be in a dedicated model object with its own class that the convertor
morph could hold on to, and which other morphs might also reference
(like an inspector morph or a charting morph or a sound-generating morph
or whatever).
Although as above, that POC is complicated by the morph subcomponent
added in the third version for text editing also having its own state
(both the text and an insertion point).
In general, any subcomponent may arbitrarily have its own local state --
or through some sort of plugability may retrieve state from elsewhere
(typically an enclosing component, but not necessarily).
> In the Cuis-Smalltalk-UI, there is a UI-Mold package to describe field
> input form, with data validation. It should be possible to add a kind of
> computed ouput object.
Thanks for mentioning Cuis-Smalltalk-UI and UI-Mold. I just imported
that and plan to poke around in it. It definitely shows the initial
value of having a wrapping system in any new approach to leverage all
those existing Cuis morphs.
https://github.com/Cuis-Smalltalk/Cuis-Smalltalk-UI
Relating to form layout, one thing missing from the Proof of Concept is
having some sort of layout manager in the "drawOn:" method.
While the quickest way to make a POC was to override drawOn:, a better
approach might be to have a new "view" method. In practice with Mithril
the "view" methods define a nested set of HTML DOM elements and also
custom components.
I'd ideally like to be able to define Cuis UIs in a way similar to this
example where I create a nested structure using arrays that defines UI
elements that are then laid out automatically:
https://github.com/pdfernhout/mithril-demos/blob/master/currency-converter/currencyConverterFew.js#L72-L91
So ultimately, I would like something more elegant than that Proof of
Concept drawOn: approach. For a form, you would essentially just create
an array or tree of all the widgets (text, editor areas, buttons, and
all the specialized variants for form input like in Cuis-Smalltalk-UI)
and where a layout manager you specified handles the details of
placement (similar to what happens in an HTML window) considering
various constraints.
There are lots of interesting layout managers in various languages that
Cuis could include -- and might already, as I have not looked for any
beyond the row and column layouts. I was fond of migLayout in Java for a
time, although with JavaScript I increasingly use the CSS Flexbox layout
approach. But improving layout managers is a different issue mostly
independent of event-driven rebuilding.
To be clear, most JavaScript developers don't know this pattern that
Mithril uses, and so they don't know how much accidental complexity they
are using when they build on, say, React with Redux. Here is a related
essay I wrote on that, explaining why Angular and React have unnecessary
complexity, which has a section on "Redraw approach" and "Data model:
Component & Services vs. Component or Redux vs. Flexible" which are
similar to the points I have made here about the observer/dependency
accidental complexity in Smalltalk:
https://github.com/pdfernhout/choose-mithril
I know it is a bit rude to come into a community and suggest a
fundamental part of a community's development paradigm could change.
Sorry about that. I do admire Cuis' quest for simplicity though -- and
all the related previous work on that -- which is why I am suggesting
these ideas here and not over on the Pharo or Squeak lists. And of
course I love the Squeak (and now Cuis) ideal of a self-reflective
self-generating system with great tools for development which now can
potentially run in any browser like via SqueakJS and also potentially
even run on bare metal without an OS at all. Having been away from
Smalltalk development for many years while journeying through
JavaScript/HTML/CSS land with all its diversity (discovering Mithril's
rebuild-on-events design pattern along the way), I feel that there is an
even more awesomely simpler Cuis possible if it discards the
observer/dependency complexity that make UI development in modern
Smalltalks more complected -- and more difficult -- than it has to be.
--Paul Fernhout (pdfernhout.net)
"The biggest challenge of the 21st century is the irony of technologies
of abundance in the hands of those still thinking in terms of scarcity."
More information about the Cuis-dev
mailing list