15 ms·
The complexity that lives in the GUI
- diggan 6y agoI'm fairly sure I've said this before here and elsewhere, but bears repeating, especially for this post. Statecharts is currently probably the most undervalued tool when it comes to programming GUIs with state. Statecharts are a continuation of state machines, but with less footguns and better abstractions to be able to build larger systems. In the end, you either build a GUI that are using state machines implicitly, or explicitly. Tends to be less prone to bugs if you do so explicitly. If you're interested, here is some starting points (copied from an older comment of mine): Here is the initial paper from David Harel: STATECHARTS: A VISUAL FORMALISM FOR COMPLEX SYSTEMS (1987) - https://www.inf.ed.ac.uk/teaching/courses/seoc/2005_2006/res https://www.inf.ed.ac.uk/teaching/courses/seoc/2005_2006/res... Website with lots of info and resources: https://statecharts.github.io/ https://statecharts.github.io/ And finally a very well made JS library by David Khourshid that gives you lots of power leveraging statecharts: https://github.com/davidkpiano https://github.com/davidkpiano While we're at it, here are some links to previous submissions on HN regarding statecharts with lots of useful and interesting information/experiences: - https://news.ycombinator.com/item?id=18483704 https://news.ycombinator.com/item?id=18483704 - https://news.ycombinator.com/item?id=15835005 https://news.ycombinator.com/item?id=15835005 - https://news.ycombinator.com/item?id=21867990 https://news.ycombinator.com/item?id=21867990 - https://news.ycombinator.com/item?id=16606379 https://news.ycombinator.com/item?id=16606379 - https://news.ycombinator.com/item?id=22093176 https://news.ycombinator.com/item?id=22093176
- bmitc 6y agoThere is an edX course that covers state charts. It’s excellent, and Davis Harel is one of the co-instructors. https://www.edx.org/course/programming-for-everyone-an-introduction-to-visual https://www.edx.org/course/programming-for-everyone-an-intro...
- diggan 6y agoThat's amazing, didn't know that, thanks a lot for sharing! Fitting that it starts today as well :)
- bmitc 6y agoNo problem! And as just a note, it starts everyday since it is a reoccurring self-paced course. But if I remember correctly, the instructors still answer questions. I really enjoyed learning about the hierarchical state machines using statecharts.
- pixel_tracing 6y agoThis isn’t really solving issues for most teams, how do you handle maintenance and tech debt of these state charts? And why only javascript example? You’re missing mobile
- diggan 6y agoStatecharts are the solution to technical debt, not the source, as you're formalizing the possible states the user can be in, and "locking" it to that. You can even apply analysis to your codebase to figure out if you're actually covering all possible states and transitions. If you take a look at the syllabus of the course, it's not about JavaScript, it's about the formalism and understanding the core concepts, without locking you to a particular technology. Whatever they are teaching in the course, you can apply to JavaScript/Swift as much as you can apply it to Desktop/Mobile. Disclaimer: haven't actually taken the course, but planning to and I've read the description of it.
- pixel_tracing 6y agoThis isn’t answering my original question, how do you get teams to adopt this? Until this process is made easy this will always just be a fever dream of fools in ivory towers
- 6y ago
- sillysaurusx 6y agoI really don't mean to be dismissive of your contribution, so please take this in the best light possible: Yuck. Use continuations with anonymous fns that tell the GUI what to do next. The state is in the closure implicitly! I wish I had an articulate counterargument, but just look at this: https://xstate.js.org/docs/#promise-example https://xstate.js.org/docs/#promise-example Statecharts are a formalism for modeling stateful, reactive systems. Beautiful is in the eye of the beholder, and never moreso when the programmer is blind to the cost of repetition and state. Arc got it right. http://www.paulgraham.com/arcchallenge.html http://www.paulgraham.com/arcchallenge.html Write a program that causes the url said (e.g. http://localhost:port/said http://localhost:port/said) to produce a page with an input field and a submit button. When the submit button is pressed, that should produce a second page with a single link saying "click here." When that is clicked it should lead to a third page that says "you said: ..." where ... is whatever the user typed in the original input field. The third page must only show what the user actually typed. I.e. the value entered in the input field must not be passed in the url, or it would be possible to change the behavior of the final page by editing the url. (defop said req (aform [onlink "click here" (pr "you said: " (arg _ "foo"))] (input "foo") (submit)))
- diggan 6y agoIn no way was that dismissive, I thank you for providing another perspective, that's always welcome in my book! I don't necessarily agree with all the implementation details of xstate, in particular to where the logic tend to be located in practice, and the reliance on the Actor model for many things in the wild. I rather try to guide people to Statecharts as a paradigm overall, and if you happen to use JS, I think xstate is probably the most mature library there. But as all libraries/frameworks, they can be over-relied upon. If you're in the Clojure/Script world, which is where I mainly locate myself, then https://lucywang000.github.io/clj-statecharts/ https://lucywang000.github.io/clj-statecharts/ is all you need and so far the library I've had the best luck with.
- sillysaurusx 6y agoI came back to edit my comment and remove the harsh words, but you left a very kind response. Sorry. This is actually a fascinating example: https://lucywang000.github.io/clj-statecharts/docs/get-started/ https://lucywang000.github.io/clj-statecharts/docs/get-start... ... because it highlights precisely my criticism. ;; define the machine (def machine (fsm/machine {:id :lights :initial :red :context nil :states {:green {:on {:timer {:target :yellow :actions (fn [& _] (println "transitioned to :yellow!")) }}} :yellow {:on {:timer :red}} :red {:on {:timer :green}}} :on {:power-outage :red} })) I just ... don't understand why anyone would do it this way. The code itself already says what to do. Adding this sort of data only subtracts from clarity with no additional flexibility. You might argue that the data model makes it flexible. But I look at that and go, that's what a function is for. The only thing you need is `(println "transitioned to :yellow!")` inside of a function called transition-to-yellow, or if you're feeling adventurous, a function called transition-to which takes yellow as an argument.
- ggm 6y agoNeither is between two choices: he's presenting Three. Small point, but it grated.
- gjm11 6y agoWilliam Shakespeare: "the scrimers of their nation, / He swore, had had neither motion, guard, nor eye, / If you opposed them." King James Bible: "For I am persuaded, that neither death, nor life, nor angels, nor principalities, nor powers, nor things present, nor things to come, nor height, nor depth, nor any other creature, shall be able to separate us from the love of God, which is in Christ Jesus our Lord." Rudyard Kipling: "But there is neither East nor West, Border, nor Breed, nor Birth, / When two strong men stand face to face, tho’ they come from the ends of the earth!" Charles Dickens: "I had youth and hope. I believe, beauty. It matters very little now. Neither of the three served or saved me." Jane Austen: "Mary's ailments lessened by having a constant companion, and their daily intercourse with the other family, since there was neither superior affection, confidence, nor employment in the cottage, to be interrupted by it, was rather an advantage." Thomas Hardy: "Half an hour passed yet again; neither man, woman, nor child returned." Samuel Johnson: "Among these, Mr. Savage was admitted to play the part of Sir Thomas Overbury, by which he gained no great reputation, the theatre being a province for which nature seems not to have designed him; for neither his voice, look, nor gesture were such as were expected on the stage" The authors above are not cherry-picked, except that I happened to remember the Kipling and St Paul quotations. Not one of the notable English writers I looked up failed to provide me with at least one example of "neither" applying to more than two things.
- ggm 6y agoMy proofreading partner disliked it also, but your proof by authority is strangely compelling.
- gjm11 6y agoI thought I'd look up a few more. Once again, every single person I tried yielded positive examples; no cherry-picking here at all. (The ones that took most tries were Macaulay and Chesterton.) One caveat: I only bothered with people for whom I expected there to be a reasonable amount available online for searching; if "neither" has recently become acceptable only when there are exactly two things, that wouldn't show up in what I found. (But I don't think it has.) John Milton: "Into this wilde Abyss, / The Womb of nature and perhaps her Grave, / Of neither Sea, nor Shore, nor Air, nor Fire, / But all these in thir pregnant causes mixt / Confus’dly, ..." Daniel Defoe: "And now I thought myself pretty well freighted, and began to think how I should get to shore with them, having neither sail, oar, or rudder, and the least capful of wind would have overset all my navigation." William Wordsworth: "Neither vice nor guilt, / Debasement undergone by body or mind, / Nor all the misery forced upon my sight, / Misery not lightly passed, but sometimes scanned / Most feelingly, could overthrow my trust / In what we may become" Thomas Macaulay: "Neither blindness, nor gout, nor age, nor penury, nor domestic afflictions, nor political disappointments, nor abuse, nor proscription, nor neglect, had power to disturb his sedate and majestic patience." Jonathan Swift: "I find likewise that your printer has been so careless as to confound the times, and mistake the dates, of my several voyages and returns; neither assigning the true year, nor the true month, nor day of the month" George Bernard Shaw: "But there are men who can neither read, write, nor cipher, to whom the answer to such sums as I can do is instantly obvious without any conscious calculation at all; and the result is infallible." G K Chesterton: "He takes us into the schools of inhumanist learning, where there are neither books nor flowers, nor wine nor wisdom, but only deformities in glass bottles, and where the rule is taught from the exceptions." Since the above are all notable English (or in two cases Irish) writers, here are a few notable Americans. Herman Melville: "But neither great Washington, nor Napoleon, nor Nelson, will answer a single hail from below, however madly invoked to befriend by their counsels the distracted decks upon which they gaze" H L Mencken: "I am thus neither teacher, nor prophet, nor reformer, but merely inquirer." Mark Twain: "It is not pleasant to see an American thrusting his nationality forward obtrusively in a foreign land, but Oh, it is pitiable to see him making of himself a thing that is neither male nor female, neither fish, flesh, nor fowl--a poor, miserable, hermaphrodite Frenchman!"
- panic 6y agoI've found the best way to handle this problem is a variation on "lift the state up", but instead of binding synchronous listeners to state changes in the model, have these state changes mark any involved views as "dirty". Then, after all events have been processed for the current run loop, go through all the dirty views and call a single update function on each. For the example given in the article, the update function could look something like def View.update(): if model.lightTurnedOn: self.backgroundColor = red else: self.backgroundColor = ibmGray This way, all view property changes happen in one place, where you can read the code and understand how the view will appear in each possible state. Circular listener loops are impossible, and view properties for animations can even be computed by calling update twice (once before and once after the state change).
- BiteCode_dev 6y agoThat's basically what modern JS frontend frameworks do as well. I suppose as soon as something becomes too complex, keeping track of all interactions is just too complicated and rerendering the entire world efficiently looks like an easier problem to deal with. I like the current trend of going back to renderless components as well. This way you separate the state changes from the way it looks like. Feels like each component is a miniature MVC framework with front and a back.
- panic 6y ago> I suppose as soon as something becomes too complex, keeping track of all interactions is just too complicated and rerendering the entire world efficiently looks like an easier problem to deal with. In fact, it can actually be less work, since you're coalescing changes into a single update() call rather than sprinkling them across observer callbacks. Also, if your update function starts running too slowly, you can always make it more precise by keeping track of which states have changed internally to the view. For example, if setting the background color takes a long time for whatever reason, you can do something like this: def View.update(): if self.lightWasTurnedOn != model.lightTurnedOn: if model.lightTurnedOn: self.backgroundColor = red else: self.backgroundColor = ibmGray self.lightWasTurnedOn = model.lightTurnedOn Now backgroundColor will only be set if lightTurnedOn actually changed since the last update.
- hermitcrab 6y agoI find that Qt signals and slots works pretty well for managing complexity in GUIs. In this case you would connect a signal that is emitted when inventory table changes state to a slot that changes the appearance in the user avatar. This would probably be done in the pane/dialog that contains them both. They 2 components would remain nicely decoupled. This approach isn't without it's own challenges of course. For example it is sometimes hard to keep track of what is going on in complex applications with cascades of signals and slots. Some people also hate the fact that signals and slots use auto generated code, but I have never really found that to be a problem in practise.
- CarVac 6y agoI agree, but isn't that just a message bus (mentioned in the article)?
- joezydeco 6y agoIt is, he's just pointing out that the Qt developers figured this out a long time ago. Web developers like to think they're pioneers when it comes to this stuff.
- hermitcrab 6y agoI never really thought of it in those terms. But I guess signals and slots are just way to publish/subscribe to a message bus. It is quite a nice abstraction of it IMHO (which is perhaps why it didn't occur to me!).
- nyanpasu64 6y agoIf one user interaction triggers a "on user interaction" signal, which causes "value changed" signals to fire, is it possible that a widget which depends on multiple of these to get redrawn multiple times? I'm optimistic about Qt 6's QProperty (I don't know how it compares to FRP or, as someone else mentioned, MobX), but Qt 6 currently does not have KDE libraries, or Linux themes to fit into desktop environments or distros.
- 6y ago
- ChrisMarshallNY 6y agoThis is a great discussion on the challenges of designing UI for complex applications. In the aggregate, what ends up being most effective for me, is rapid prototyping, and “paving the bare spots.”[0] I find that I do a terrible job of predicting user mental models. The rub with prototypes, is that they can’t be lash-ups, as they inevitably end up as ship code. This means that a lot of good code will get binned. It just needs to be accepted and anticipated. Sometimes, I can create a standalone project for code that needs to go, but I still feel has a future. So there’s always a need for fundamental high quality. What has been useful to me, is Apple’s TestFlight[1]. I write Apple software, and TestFlight is their beta distribution system. I start a project at a very nascent stage, and use TestFlight to prototype it. I use an “always beta” quality approach, so the app is constantly at ship quality; although incomplete. It forces me to maintain high quality, and allows all stakeholders to participate, even at very early stages. It’s extremely motivating. The level of enthusiasm is off the charts. In fact, the biggest challenge is keeping people in low orbit, so they don’t start thinking that you are a “WIZZARD” [sic]. It also makes shopping around for funding and support easy. You just loop people into the TestFlight group. Since the app is already at high quality, there’s no need for chaperones or sacrifices to The Demo Gods. I like that it keeps development totally focused on the actual user experience. They look at the application entirely differently from me. [0] https://littlegreenviper.com/miscellany/the-road-most-traveled-by/#paving https://littlegreenviper.com/miscellany/the-road-most-travel... [1] https://developer.apple.com/testflight/ https://developer.apple.com/testflight/
- tebbers 6y agoGreat post and that first link is brilliant, thank you for sharing. I agree with you regarding your attitude to developing software - just get it out there first and then adjust the UX later.
- pixel_tracing 6y agoArticle presents a bunch of problems on UI but no solutions... perplexing.
- thijsvandien 6y agoThere already are too many (supposed) solutions that came about before properly understanding the problem, which is extremely valuable in and of itself.
- MaxBarraclough 6y ago> Clicking on buttons will start triggering events which will modify the state in the model that will in turn start triggering event listeners causing your GUI to flash like a christmas tree. The problem of data bindings and change listeners is that they make it really easy to introduce a hidden circular event listeners that will trigger one another multiple times (event A changes state B and change of state B triggers event A). Agree with both of these points. You can no longer treat assignment simply as the way you mutate data, you also have to anticipate its effects in the data-binding system. I imagine the circular event problem could be addressed with static analysis, but I don't know of any framework that does this.
- amelius 6y agoIsn't this what React and precursors were invented for?
- MaxBarraclough 6y agoI have to plead ignorance here. Does it have a way of detecting circular events?
- amelius 6y agoThe idea is to separate the state of the application from the presentation, which in practice leads to not having circular dependencies in the first place. And you can update the display after all changes are finished, so you won't get the flashing xmas tree effect.
- TeMPOraL 6y agoI thought TFA meant flashing in the metaphorical sense - it's the code paths that light up, not things on the screen. Circular dependencies are an unfortunate fact of life, and I wish we had tools to deal with them, instead of desperately trying to avoid them. A good test for a UI paradigm is, how do you handle a UI like this: A = [50] [====| ] B = [10] [| ] C = [60] [=====| ] A + B = C Where [===| ] things are sliders, all three are changeable, and the relation A+B=C must always hold.
- hyberbole_1234 6y agoThe best approach I’ve seen is separating State and Views and having some form of property diffing. struct LightState { var isLightOn: Bool } class LightViewController { let lightView = UIView() } class StateDirector< LightState, LightViewController> { let lightState: LightState init(state: LightState) { self.lightState = state } func bind( view: LightViewController ) { // Every time is turned on changed call this LightState.add( listener: self, for keyPath: \.isTurnedOn, handle: .method(Self.handleLightChange) } func handleLightChange(isOn: Bool) { view.lightView.backgroundColor = isOn ? .green : .red } } This allows clear separation of State, View and Changes. You can then just model your state in your reducer and “simulate your views” inside Unit tests
- chris_wot 6y agoUm, isn't this what the Mediator pattern was designed to solve? https://en.wikipedia.org/wiki/Mediator_pattern https://en.wikipedia.org/wiki/Mediator_pattern
- flohofwoe 6y agoIME, working with an immediate mode UI framework automatically gets rid of most such "architecture astronaut" problems. But I found that it's almost impossible to describe to someone used to event-/callback-driven UIs why exactly that is. You really need to try it yourself on a non-trivial UI to "get it".
- amelius 6y agoYes, but immediate mode causes more CPU load (or draining of batteries) since you have to redraw everything all the time.
- panic 6y agoIt's easy to skip drawing frames when no state changes, and for frames where state does change, you can use techniques like rxi's cached software rendering to avoid redrawing the entire screen: https://rxi.github.io/cached_software_rendering.html https://rxi.github.io/cached_software_rendering.html
- amelius 6y agoHow does it handle scrolling? This is typically very fast and smooth (performed by bitblt and redrawing only the newly exposed part on every frame), but in immediate mode you'd have to redraw the entire screen on every frame.
- panic 6y agoModern renderers typically draw scrollable content into a separate texture (or collection of smaller textures that tile the scrollable region) and use the GPU to scroll rather than bitblting on the CPU. You can use the same technique to render the scrollable part of the UI in an immediate mode system.
- flohofwoe 6y agoI think this whole efficiency thing is a common misconception. Only the public API appears "immediate mode", the internal implementation doesn't need to be, and usually isn't (e.g. it keeps mutating state between frames instead of building everything from scratch again). The user-side code basically describes what the UI should look like in the current frame, those "instructions" are recorded, and this recording is reasonably cheap. The UI backend can then figure out how to "diff" the new instruction stream against the current internal state and render this with the least changes to the screen. However some immediate mode UI systems came to the conclusion that it might actually be cheaper to just render most things from scratch instead of spending lots of processing resources to figure out what needs to be updated. In conclusion: "Immediate Mode UI" doesn't say anything how the UI is actually rendered or generally how the internals are implemented, it only describes how the public API works.
- mwcampbell 6y ago> Immediate Mode GUIs somehow never reached the critical mass and you are probably not going to find it outside of the games industry. And that's a good thing, because so far, AFAIK, no one has implemented accessibility (e.g. for screen readers) in an immediate-mode GUI. I hope to work on that problem sometime soon.
- rwmj 6y agoI remember in my first real job I wrote a GUI for an RTOS (all the way up starting with the hardware, device driver, ...). Not knowing anything about how GUIs worked, or about events, I had a main loop which redrew the GUI on every action. This article tells me I wasn't completely wrong, these are called "Immediate Mode" GUIs!
- codeflo 6y agoI’ve recently become interested in immediate mode UIs, and find that there are surprising similarities to React. In both cases, components are just functions that must explicitly render their child components and pass down any shared state. It’s conceptually a very clean way to handle UI state. However, React introduces a lot of complexity to avoid unnecessary DOM updates, which makes me wonder about the viability of an immediate mode GUI in the browser using canvas.
- wdfx 6y agoI've worked on an app with an IMGUI on canvas. It was amazingly fast and responsive. Nothing which was built after that to try and replace it was anywhere near as performant.
- TeMPOraL 6y agoGiven how IMGUI is a bunch of branches with code that redraws the same pixels all the time, 60x per second, I'm still surprised this is considered very (if not most) efficient UI. It would imply retained-mode UI frameworks are strongly undershooting their theoretically possible performance.
- flohofwoe 6y agoSee for yourself ;) https://floooh.github.io/sokol-html5/imgui-highdpi-sapp.html https://floooh.github.io/sokol-html5/imgui-highdpi-sapp.html There are also demos for other immediate-mode UI systems on the parent page (Nuklear and microui): https://floooh.github.io/sokol-html5/ https://floooh.github.io/sokol-html5/ As I wrote in another thread, Immediate Mode UI doesn't imply how the UI rendering is implemented, only how the API works.
- fassssst 6y agoThe problem, as always, is how to make immediate mode stuff work with accessibility tools. The value of essentially creating and mutating a tree structure like the DOM is that things like screen readers and UI automation tools can read it. With canvas they just see a big bitmap.
- FpUser 6y ago>"the GUI always ends up being a ridiculous mess" Well no. There are applications with nice well thought out GUIs. >"Congratulations, a large amount of your effort will go towards resolving weird message bus problems as opposed to writing the business logic of your app" Sorry but I do not resolve "weird message problems". I use my own publish-subscribe mostly asynchronous message bus for my GUI apps (actually I use it also for non GUI parts as well). Components (visible or not and including running threads) can subscribe to events. It does not exhibit any performance / memory problems and in combination with the global app state object makes programming interactions a piece of cake.
- nikisweeting 6y agoEvery codebase is clean and elegant when in the head of a single person. How big is your codebase and how many people work on it?
- FpUser 6y ago>"Every codebase is clean and elegant when in the head of a single person." Simply not true. I recently had to salvage business rules from a codebase written by single person over the course of many years. It was probably one of the messiest code I've ever seen. >"How big is your codebase and how many people work on it" Depends on a project. On some I work alone. Some had 2-3 persons. The biggest team I've ever had to lead was about 35 people (not all developers). Properly organizing and splitting work and using mostly experienced people the resulting code was very decent and quite possible to grasp. Also well documented.
- nikisweeting 6y agoI don't mean objectively clean and elegant, I mean in the eye of the beholder it's easy for a tiny codebase to be clean. But a system that may be clean and elegant for a 1-person gig may end up falling to bits if you attempt to have 100 people working on it.
- c-smile 6y agoTL;DR: there is no one silver bullet for the UI in general. As a rule practical and manageable UIs use all these approaches at the same time. Components, class based and/or functional (fe: UserSection, InventoryTable ), are in principle loosely coupled. They may not know about each other and use messages to communicate. The Flight by Twitter ( https://flightjs.github.io/ https://flightjs.github.io/ ) was/is probably the first practical thing that pioneered that approach. The Flight is not even a framework but rather a design principle. In fact it is an anti-framework - it postulates that in practice each component may have its own optimal internal architecture. One component is better to use React-ivity, another - Angular-ish data binding, etc. On some, immediate mode drawing is the most natural. Smaller the task (component) - greater the chance to find silver bullet for it.
- phtrivier 6y agoThis seems to miss the 'ui=f(state, effects)' model that's sort of what's behind react+redux or elm or similar ideas. Or is that really the same as immediate mode ?
- continuational 6y agoI ran into a buch of similar problems in the past personally, so much that I ended up writing my own library. The home page is a bit ugly, but it contains a range of examples that are commonly awkward to implement in other UI libraries: http://www.react4s.org/ http://www.react4s.org/
- dale_glass 6y agoWhat kind of GUI app has a performance problem with the message bus? We're in the age of 4K, 60 FPS rendering. If any GUI application has a message bus that's strained enough to impact performance, then either the application isn't made for humans (because if all that stuff is doing anything it'd result in a screen updating far faster than it could be read), or there's some horrible bug somewhere that produces a flood.
- rwmj 6y agoIn reality none, but developers often think it's not a "real" message bus unless you have to install it on a separate cluster of machines (like AMQP), or it breaks your desktop randomly (dbus). The idea that a message bus could be part of the application and very lightweight is unexpected.
- spion 6y ago> I still don’t know what the proper solution to this problem would be. Keep your state manipulations as simple as possible and try not to share any data between different models. Every time I went forward with some fancy listener-binding mechanisms, I’ve ended up causing subtle circular listener recalculations that were extremely hard to debug. The answer for me has been pervasive use of MobX computeds everywhere. See https://mobx.js.org/getting-started https://mobx.js.org/getting-started
- jonathanstrange 6y agoHere is my personal opinion on it (not sure if it's the right one, though): Write the GUI independently of the model, let the GUI components update and communicate among each other as they like, and perform some validation in these GUI components. Translate between model and view only at one well-defined point at which there is also a final validation at the model side, and make sure model and view are only loosely coupled. Good GUIs have way too many requirements to be controlled (via a controller) by the model. As a typical example, the fact that a button should only be activated if there is data in a textbox has usually nothing to do with the underlying model. The model should only ever contain valid and consistent data.
- kaoD 6y agoThis sounds exactly like what I settled on as React+Redux best practices. There's UI logic+data (that belongs in components) and business logic+data (that belongs in the Redux store) loosely coupled via actions and selectors.
- bob1029 6y agoThis is basically our approach with Blazor now. I am not sure Microsoft has even picked up on or documented our pattern yet, but we do something where: 1) Define a domain model + service(s) that fundamentally addresses the logical business functionality without any notion of a specific UI or its stateful nature. This service should be written in terms of a singleton and injected as such. This is something that should be able to be 100% unit testable, and could be exposed as a prototype in a console application or back a JSON API if necessary (i.e. pretend you are writing a SAAS). 2) Define UI state service(s) that take dependency on one or more domain services. These should be scoped around the logical UI activity that is being carried out, and no further. The goal is to minimize their extent because as we know more state to manage means more pain. These would be injected as Scoped dependencies (in our use of Server-Side Blazor), such that a unique instance is available per user session. Examples of these might be things like LoginService, UserStateService, ShoppingCartService, CheckoutService, etc. These services are not allowed to directly alter the domain model, and must pass through domain service methods on injected members. Keep in mind that DI is really powerful, so you can even have a hierarchy of these types if you want to better organize UI event propagation and handling. 3) Define the actual UI (razor components). These use the @inject statement to pull in the UI state services from 2 above. In each components' OnRender method, we subscribe to relevant update event(s) in the UI service(s) in order to know when to redraw the component (i.e. StateHasChanged). Typically, we find that components usually only inject one or 2 UI state services at a time. Needing to subscribe to many UI state events in a single component might be a sign you should create a new one to better manage that interaction. This is our approach for isolating the domain services from the actual UI interactions and state around them. We find that with this approach, our UI is quite barren in terms of code. It really is pure HTML/CSS (with a tiny JS shim) and some minor interfacing to methods and properties on the UI state services. This is the closest realistic thing I've seen so far in terms of the fabled frontend/backend isolation principle. Most of the "nasty" happens in the middle tier above, but it is still well-organized and easy to test. By enforcing immutability on the domain services, we ensure discipline with how the UI state services must consume the model. Blazor then goes on to eliminate entire classes of ridiculous bullshit by not forcing us to produce and then consume an arbitrary wire protocol to get to our data.
- protoman3000 6y agoA related question, how do GUIs actually resolve quickly on which element under the cursor a click lands? How is the click propagated recursively through every component and is the position compared repeatedly at every step?
- pixel_tracing 6y agoI believe this can be resolved using a quad tree to resolve point locations and map them to a view in a view hierarchy
- badsectoracula 6y agoThey just enumerate all child windows, these events are not frequent enough for this to be a performance bottleneck. The check is something along the lines of Win* GetChildAt(Win* w, int x, int y) { size_t i; if (x >= w->ScreenX1 && y >= w->ScreenY1 && x <= w->ScreenX2 && y <= w->ScreenY2) { for (i=0; i < w->ChildCount; i++) { Win* child = GetChildAt(w->Child[i], x, y); if (child) return child; } return w; } return NULL; } Call this on the root window and you have the deepest child at the given coordinates. (though there is usually a bit extra logic for handling, e.g., invisible and disabled windows)
- walkingpigeons 6y agoAt the end of the article it said something about immediate mode. I do think React or Vue are kind of working like this now? Both requires you define your state of the each component and define how it should look like based on the state values. When you update the state it will then update the view automatically as well based on what you've defined (JSX/template). It is not 30/60FPS though, it is re-rendered when it knows there is a state change (i.e. setState is triggered)
- IshKebab 6y agoThis is a really good take on the (still unsolved IMO) problem, though I suspect it only makes sense to people who have experienced all the issues personally, since so many of them are "it gets really awkward with big programs" type problems. He did miss out a pretty significant flaw of message busses - the producers and consumers of messages are completely decoupled, which makes debugging really annoying because you don't have anything like a stack trace. You just know that your component received a message, and good luck trying to figure out where it was sent from and why. That's also a big problem with magical reactivity systems like Vue. Your stack trace is pretty much "yeah something changed somewhere so we're updating this component. Don't try and figure it out."
- RoyalSloth 6y ago> the producers and consumers of messages are completely decoupled, which makes debugging really annoying because you don't have anything like a stack trace This is actually a really good point. I don't know why you were downvoted.
- pixel_tracing 6y agoThere’s always a stack trace. The question is figuring out deep your eventing layer is. Also dispatching an event on one thread and receiving it in a component on another thread essentially negates the trace in this sense too (sometimes depending on your environment). A solution I’ve always had is to build your message bus with logging in mind initially
- IshKebab 6y ago> There’s always a stack trace. I think you misunderstood. Of course there's always a stack trace; you're still executing code. But with message buses and magic reactivity systems your stack trace always just goes to `mainEventLoop()` or `processEvents()` or whatever. It doesn't go to the thing that actually caused the change as it would if you used direct function calls. I'm not saying it's a deal breaker, it's just a notable downside of those architectures.
- 6y ago
- mattgreenrocks 6y agoI’m not fond of the reasoning employed in the message bus section that decries the fact that misuse results in a big mess. No programming paradigm can stand up to rushed/flawed mental models. The domain can become quite complex; it is wishful thinking to believe that a single approach could drain it of all complexity.
- brundolf 6y agoThe fundamental challenge of GUIs is that they have state as a core concern. Unlike most systems, state is not an implementation detail that can be refactored away. It is central to the problem domain. The end user sees and cares about having a stateful system. The hardest part of dealing with state in a complex system is maintaining consistency in different places. Some instances of this this can be avoided by creating single-sources-of-truth, but in other cases you can't unify your state, or you're dealing with an external stateful system (like the DOM), and you have no choice but to find a way to keep separate pieces of state in sync. I should probably write a blog post on this
- rwmj 6y agoThe way Tcl/Tk dealt with this was nice: You could watch the value in a variable, getting notified when the variable changed. GUI controls could therefore exactly reflect the content of variables. Of course this comes with a certain hidden overhead. This complete example will toggle the checkbox every second (1000ms), or the user can click to update the variable. The checkbox watches variable "v". #!/usr/bin/wish checkbutton .button -variable v -text "Click me" pack .button proc run {} { global v after 1000 run set v [expr !$v] } run
- brundolf 6y agoMobX is the equivalent for the space of reactive web frameworks, and I agree it's a fantastic model. The cost is that it requires a little "magic", but the benefit is that the entire question of synchronizing state virtually disappears, leaving you to focus on modeling and controlling state (which are still nontrivial)
- trulyme 6y agoNo. Just no. You simply can't use "MobX" and "fantastic" in the same sentence. There are just so many problems with MobX. For example reactions - ever tried figuring out why some value changes in a bigger app? Not to mention abysmal tooling (compared to Redux DevTools). But the biggest problem is that everything is... sprinkled with magic. Just give me Redux(-toolkit), thank you very much, I can understand these much more deeply. /rant If I sound confrontational, sorry about that... I just had the misfortune of inheriting a MobX app. Magic indeed.
- deleted 6y ago[deleted]
- deleted 6y ago[deleted]
- cpill 6y ago> I’d love to hear what the functional programming camp has to say about this problem, but I guess they are too busy with inventing yet another $20 term for a 5 cent concept hA! Hit the nail on the head :P
- heycosmo 6y agoI have a simple rule for GUI design: build trees not graphs. Write components that accept a state snapshot and broadcast changes. If component A listens for state changes from B, then A is a parent node of B. If A sends state to B, then A is a parent of B. Components reconcile state before broadcasting changes toward the root of the tree. Often there is a price paid in brevity, but I believe it is worth it. It may seem annoying to propagate a click explicitly through 5 parent components just to sum clicks into a count widget, but as soon as a short circuit is made, you've created a graph, and you lose the ability to isolate GUI sub-trees for testing/debugging.
- nikisweeting 6y agoThis makes visual redesigns take foreeeeever though. Imagine moving a component from the main area of your app into a menu dropdown in the navbar, now you have to tear out all those props that you painstakingly passed down through 10 intermediary layers.
- madhadron 6y agoI mean, of course you lift the state. It's just like a database. You have a normalized model containing the state, and the GUI is a view on it. Events from the GUI trigger transactions that update the state and then every element is triggered to update itself based on the new state. That should never trigger a loop of events, because a view updating itself from the model should never trigger transactions on the database. Many complicated views have their own internal models for things like where they are scrolled, what columns are shown, or what elements of a tree are expanded. But those compound views are written so that, from the outside, they appear exactly the same as any other view. > I’d love to hear what the functional programming camp has to say about this problem It's called functional reactive programming.
- nikisweeting 6y agoThis is the key line that the original article is missing imo, which solves much of the headache that they claim exists with message passing/raising state: > a view updating itself from the model should never trigger transactions on the database i.e. a view rendering from state/messages, should never re-emit new state/messages.
- Chris_Newton 6y agoI thought the article was too dismissive of concepts like MVC, immediate mode and functional programming, given that ideas coming from those directions might offer the answers the original author is searching for. The separation of an underlying data model from any particular way of presenting its current state is a powerful idea that has proven its value many times. We’ve used it to build user interfaces, from early GUIs when we didn’t yet have the kinds of libraries and platform APIs we enjoy today, through games with unique rendering requirements and a need to keep frame rates up, to modern web and mobile app architectures. The same principle is useful for building non-user interfaces, too, in areas like embedded systems where you are “presenting” by controlling some hardware component or distributed systems where you are “presenting” to some other software component as part of the larger system.
- erdo 6y ago
- onion-soup 6y agoDon't libraries like react solve this?
- tomaszs 6y agoReact makes it even harder actually. Angular has some ways to help. Ember has it. Vue - not so much
- filoeleven 6y agoCan you qualify this? My experience has been the exact opposite. Angular rewrote their entire framework to be more one-way data-binding focused because the two-way in AngularJS was a nightmare. They still have a weird mechanism to allow it, and they still have lots of wiring all over the place. Modern React, with hooks and functional components, solves the problem posed in the article by choosing option 2, lift the state up. The hypothetical problem (change the avatar background when the “working” light is on) is a non-issue. You wouldn’t add an event listener for the light’s state change, you’d simply pass the “isWorking” prop to both the light and the avatar, and they would each render based on the value of “isWorking”. There is no reason for one component to know about the other; they don’t actually do anything except sit there and look pretty, correctly. The UI is always backed by a data model. The more explicitly you express that model—by keeping it all in one tree like Redux does, for instance—the simpler your UI becomes to reason about. React won’t (can’t) stop you from doing bad things like hitting random services when a component loads, but its design, especially recently, guides you away from that pitfall.
- tomaszs 6y agoRegular binding in Angular is straight forward: you can pass params to child component and you can bind to events. Child does not know about parent by design. So it is a good practice baked in Angular. If it comes to the question. I think you have answered yourself by writing React won't stop you from making bad design decisions.
- 6y ago
- nikisweeting 6y agoExcellent writing, this is a great article to show a junior frontend engineer who would otherwise spend their next years having to crystalize these ideas on their own. I wish I had read this 8 years ago.
- smhg 6y ago> There is also another way of making GUIs called Immediate Mode that is commonly used for drawing user interfaces in games. In this mode the GUI components are no longer subscribing and waiting for events to come, but are instead a part of the main loop that runs at 60 fps and re-render themselves based on the current “global” state. > Immediate Mode GUIs somehow never reached the critical mass and you are probably not going to find it outside of the games industry. Isn't this what React is built on? I think this was part of the 'original' selling point by Pete Hunt: https://youtu.be/x7cQ3mrcKaY https://youtu.be/x7cQ3mrcKaY (2013). Around 20:00 in that video he compares React with the Doom3 engine.
- jonathanaird 6y agoYes, Flutter does this as well and they’re both very popular. When done properly, UI is a pure function of state and you have some simple mechanism for notifying the UI that state has changed.
- millstone 6y agoHow does this work with e.g. a text editor? It doesn't seem practical to have your UI be a pure function of an input which is 100+ MBs of text and formatting.
- jonathanaird 6y agoBoth Flutter and React will take the next UI frame and figure out what the diff is between the last frame and the next and only apply the difference. In React it’s called the shadow dom. Flutter will also only render what it needs to so you shouldn’t render the entire file at once. Just render what’s on screen and lazy load as you’re scrolling.
- deleted 6y ago[deleted]
- strictfp 6y agoYou can also use the original web page model; the state is on the server, every button click generates an action to change state, and the updated ui is regenerated fron scratch based on the new state.
- vikingcaffiene 6y agoI've written front end for nearly all of my 13 years in this field. I've written some absolute dumpster fires in my time and have also written some very large and complex UI's successfully. I would like to think I have some insight to add here. I agree with a lot of the points brought up in this article. I'd like to add a few more points about why I think FE can be such a pain to get right: 1. Mixed concerns and a lack of application layering. In React code bases and others like them, its not uncommon for me to find business logic embedded directly inside the components themselves. This makes the component inherently coupled to the specific implementation its being used and can only be re-usable in other contexts by passing in modifier flags to the props etc. In my opinion, components should be mostly of the stateless flavor and delegate anything not directly related to their immediate UI concerns elsewhere. This increases the likelihood of re-usability of your components and makes testing business and component logic much much more straight forward. 2. This might just be my personal experience, but I've noticed a bit of a dismissive attitude around design patterns and traditional computer science principles among my front end brethren. YAGNI and all that. While I think its fair that the UI != to the back end, I think frequently the baby gets thrown out with the bath water. For instance, I frequently leverage dependency injection in my front end work as a way to make my code more flexible and testable. It's hard to sell the benefits of something like DI until your application grows in complexity to the point that you are dealing with things like nested mocks in your tests and/or need to make a non trivial change to some piece of functionality. I've been seeing the winds start to shift a bit on this which is encouraging. 3. Most of the time there is little to no overall _conceptual integrity_ to a given front end code base. Its uncommon for me to come into an existing code base and have anyone be able to tell me what the architecture is comprised of. Things like what logic lives where and why. I'm not saying people _don't_ do this, but in the more gnarly code bases I've encountered, this "accidental architecture" is more common. 4. Front end is still see as "easy" and the place you stick the less experienced engineers first. I sincerely hope this doesn't come off like I am gatekeeping or anything. I work with some absolutely brilliant people who are only a year or two into their career and much smarter than me. IMO its less about skill and more about having been burned enough times to know where unexpected complexity lie. I love front end. Its challenging and interesting and maddening. My hope is that it will continue to mature and grow to the point that it gets the same respect as other disciplines within this field. These days I work full stack so its not all I do, but it will always be my favorite. :)
- tomaszs 6y agoAfter 20 years of working with frontends mostly I consider that there are three causes of problems with GUI: - it is badly designed. If state is too hard to manage it means often that the design is bad - state is not the most important part of the UI. Interaction and being elastic to changes are the most important things - frontend is often overengineered in a bad way. When you use bad solutions like React or Redux there is no change there won't be any problems.
- valand 6y agoAnother option is injecting an optional dependency without connecting the box. I see OP doesn't mention this option, but is slightly related to both option 1 (connect the box) and 3 (message bus / event emitter). This option is similar to how OS provides an API to user space application program. For example Windows provides an API to an application to flash its window without exposing all of its state like mentioned in option 1. https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-flashwindow https://docs.microsoft.com/en-us/windows/win32/api/winuser/n... Here's the detail: A self-powered object, let's call it workingIndicatorObject, can be introduced to work on the working indicator. It provides 1.) `getState() -> WORKING/NOT-WORKING` a function to get its state, and 2.) `register() -> None`, a function to register user activities. These functions are dependencies for UserSection component and InventoryTable component respectively. In term of lifetime and hierarchy, it precedes and outlives both components. The workingIndicatorObject MUST have its own lifecycle to regulate its internal state. Its core lifecycle MUST NOT be managed under the same event loop as the GUI components, assuming the program is written on top of a framework that has it (React, Vue, etc). This is to assure that it doesn't directly affect the components managed by the framework (loose coupling). Although, a binding MAY be made between its state and the framework-managed event loop, for example, in React environment, wrapping it as a React hook. An EventEmitter can also be provided for a component to listen to its internal event loop. Injecting it as a dependency can use features like React Context or Vue's Provide/Inject pattern. Its consumer must treat it as an optional dependency for it to be safe. For example, the UserSection component can omit drawing the "working" indicator if it doesn't receive the `getState` function as a dependency. Internally, the workingIndicatorObject can use a set. The set is used to store a unique value (a Symbol in Javascript). `register` function creates a Symbol, stores it in the set, and deletes it after $TIMEOUT, while firing event both at addition and deletion. `getState` function is a function that returns `set.size()`. When bound to the component, an addition to the set fires an "update" command to the component, which redraw the component along its internal state. This is just one example of implementation and there are other simpler ways to have the same behaving workingIndicatorObject. This approach allows UserSection and InventoryTable to only know `getState()` and `register()` function and nothing else, and having both of them optional. Using static type system such as TypeScript can help a lot in this since we can pin down the signatures of both function to `null | () => WORKING | NOT_WORKING` and `null | () => void`, where we can enforce both dependent components to check if it exists and to call it with the correct types, otherwise the compiler yells.
- erdo 6y agoSo nice to see this universal issue being discussed, it doesn't seem to get as much attention as it deserves. IMO, a great solution here is along the lines of "Lift the state up" / "MV(X)". But... there is a vital detail which is usually missed when deciding how exactly the state in your M gets passed to your V for display: you must refresh your entire V when M changes in any way, not just the bit of V that you think changed. It's the only way to completely remove these difficult to test, hard to spot edge cases that the article discusses. This is almost impossible to talk about without specific examples, so a while back I wrote such an example that I think distills the core problem, and demonstrates how refreshing the entire V not only solves the problem but typically takes less code to do it: https://dev.to/erdo/tutorial-spot-the-deliberate-bug-165k https://dev.to/erdo/tutorial-spot-the-deliberate-bug-165k
- RoyalSloth 6y agoAuthor here. This is not related to this discussion, but does anybody know what causes these lines to appear in my server logs? <REDACTED> - - [14/Feb/2021:22:26:45 +0100] "GET /posts/the-complexity-that-lives-in-the-gui/ HTTP/1.1" 200 16798 "-" "HackerNews/1391 CFNetwork/1220.1 Darwin/20.3.0" <REDACTED> - - [14/Feb/2021:22:26:45 +0100] "GET /posts/the-complexity-that-lives-in-the-gui/ HTTP/1.1" 200 16798 "-" "HackerNews/1391 CFNetwork/1220.1 Darwin/20.3.0" <REDACTED> - - [14/Feb/2021:22:26:45 +0100] "GET /posts/the-complexity-that-lives-in-the-gui/ HTTP/1.1" 200 16798 "-" "HackerNews/1391 CFNetwork/1220.1 Darwin/20.3.0" The requests usually come from a certain ip multiple times until fail2ban bans it. It's not just one offender, there are multiple behaving like that.
- tn1 6y agoIt's probably an iOS app client for HN. CFNetwork is like their URLConnection I think. You're blocking people who want to read your article! As for why it occurs so often in quick succession, perhaps there's a bug in the app causing it to fetch several times instead of once.
- RoyalSloth 6y agoThanks, I thought it was a bug in some app and just wanted to be sure, so I don't have to babysit the server. If anybody knows which app is that, please tell the maintainer that they have a serious bug. It's night here, so I am logging off.
- 1vuio0pswjnm7 6y agoI rarely ever use a GUI. Certainly not for any recreatonal web use. Unless I'm using a GUI, I do not load the graphics layer. I stay in textmode. No X11, Wayland or whatever is compiled in. Ideally I have a designated computer on the local network that has a graphics layer, a full corporate GUI OS. When I need graphics to view stuff I can send it over the local network to the GUI computer. Otherwise I keep recreational use on text-only computers, away from the whiz-bang corporateOS-controlled computer.
- Sophistifunk 6y agoCool story bro
- scarredwaits 6y agoGood explanation, but the article reads as if React never happened. Maybe it was written by someone who's only worked on desktop apps?
- cdaringe 6y agoAgreed. A nice concise way to summarize my gut synopsis.
- axilmar 6y agoI totally disagree with the author. For me, the problem of state management in the GUI is totally solved: the GUI shouldn't manage state, plain and simple. If you have a problem synchronizing your views with a light, that's because the light should exist outside of your views. MVC was invented in the 70s. Why do we have to act that this is not a solved problem?