I think the main issue is the functional approach. Having a functional approach to state can be quite elegant, but ultimately the computer does not do functional. You have to implement a lot of plumbing to have stuff that is a bit performance (clojure), or just add a veneer of functional over what is essentially a normal state machine (emacs).
React works well, because it's only an abstraction over the real DOM. React only handles your app state. But the DOM mechanism is still very performant and very much imperative. But I don't think something like React would work well in a mobile app, because the UI tree is often very simple on iOS and macOS.
UI is also very much not functional, and in fact the lack of progress in UI the last 30-40 years can largely be traced to trying to create UI with procedural/functional programming languages, an instance of linguistic-architectural mismatch.
Further reading:
Programs = Data + Algorithms + Architecture: Consequences for Interactive Software Engineering -- Stéphane Chatty.
"A view is a (visual) representation of its model. It would ordinarily highlight certain attributes of the model and suppress others. It is thus acting as a presentation filter."
View and model are related, but neither is procedurally dominated by the other. The view is not a subroutine of the model, or vice versa.
They are related entities that communicate in order for the view to function as a representation of its model to the user, and for the model to be manipulated by the user.
To get the details I really recommend the Chatty paper. It is a bit hard to read, but delivers the goods.
That's pretty much where immediate mode GUIs and to a less extent React came from. (Though for React the provenance is probably closer to the HTML web-app: send request - update model - return HTML with complete and completely new UI.)
The difference is that for most games, throwing away the complete rendered graphics and re-rendering from the world model is often the right approach.
For most UIs, it isn't, unless they really are very close to video games, for example mostly passive feed readers, video players etc.
> The difference is that for most games, throwing away the complete rendered graphics and re-rendering from the world model is often the right approach.
Well, that is what "retained mode" GUIs (i.e. those using control/widget trees and such) do too.
The immediate mode GUI to me always felt like an approach came up by people who do not really want to bother doing GUIs. In terms of games it is like mixing logic and presentation in a game like, e.g.
int x, y;
while (game_running) {
draw_sprite(player, x, y);
if (key_down(LEFT)) x -= 1;
// etc
}
basically like in a game that doesn't have a world model with entities etc, just draws sprites directly, handles input directly, etc. Which is very simple for simple stuff but doesn't scale as complexity increases.
Same with immediate mode GUIs and i don't think it is a coincidence that the more complex an imgui gets, the more it starts looking like a "retained mode" GUI and the gnarlier the code becomes.
This is an optimization that some GUI systems do but it is not inherent to "retained mode" GUIs, does not apply to all systems nor all systems use it. I remember GUI systems even from late 90s/early 2000s composing their widget trees afresh when needed (and in games it was pretty much always the norm for GUI systems to work like that after engine started taking GPUs for granted). Some would limit the refresh to widgets overlapping the damage region reported by the OS (if a compositor was not running, for OSes since Vista) but even that is just an optimization.
On the other hand immediate mode GUIs are inherently like that because that is their core premise.
At each point you reify the result into an artifact that is everything the next stage needs. This has a couple of nice properties. One property is that everything is deterministic and testable. Another property is that after the pack and apply, everything is now embarrassingly parallel. Another good property is that you have an artifact that the accessibility people can latch onto before you bury it under pixels.
That would be a very functional way to deal with GUIs.
However, we continue inheriting properties and single main threading everything like we're still on 33MHz machines with 8MB of RAM.
You are applying a one way arrow to events where in real life events do change state and can completely change UI (and functions) itself. Which makes imperative always the superior mode.
What you described works only in super simple scenarios. Whink Web 1.0, when Javascript was used for basic form validation at best, and didn't change DOM that much.
1. What does that even mean? I don't see a UI in there at all, at best some graphics (Render).
2. That's not "functional". If anything, it looks like a pipeline, so dataflow. But then again, see (1)
3. Not sure why reification, that is turning things into objects, is functional to you. Reification, that is turning things into objects, is object-oriented.
4. MVC was actually created on 5.8 MHz machines with 128KB of RAM (including the display buffer). And still works beautifully today.
Functional is all about reification. You take a set of things in, you apply/map/collect/fold/whatever, you eject a set of things out. That is 100% functional--every time you supply the same inputs you get exactly the same outputs. The point of functional programming is that you avoid mutation and hidden state.
And, um, side note: dataflow programming is almost always considered functional programming.
Object oriented, by contrast, is all about hidden state and mutation. I send a message or call a function on that object over there and fingers wiggle and magic happens. But I don't know what or when or even if it actually happened.
And increasing evidence suggests that MVC doesn't work beautifully today because it doesn't parallelize, and it scatters state between uncooperative things. Just ask every single GUI that exists--every single one hangs because somebody made an oops and put too much work on the single, blessed primary thread. Or they have janky scroll. Or resizes leave gunk on the screen. Or your system flashes and jiggles because it's too busy reflowing everything in the universe. Or ...
You obviously have some very non-standard definitions at work here. apply/map/collect are just higher order operations, they have nothing to do with reification, except that you need the functions that are arguments to be first class.
> dataflow programming is almost always considered functional programming
That turns out not to be the case. Dataflow programming shares some aspects with functional programming, they are not the same at all.
> Object oriented, by contrast, is all about hidden state and mutation.
That also turns out not to be the case at all. Heck, there were even object-functional programming languages.
> And increasing evidence suggests that MVC doesn't work beautifully today because it doesn't parallelize,
What does MVC have to do with parallelization, in your humble opinion?
Famous UI = f(Model) is oversimplification that was sold in slides. Real “functional UI” frameworks implement UI = f(Model, UIState) where UIState is scroll and cursor positions, view pool for virtualization, rendering caches, etc. USState is mutable and managed by the framework and the rendering engine (e.g. React + DOM, SwiftUI +
UIKit + CoreAnimation). I don’t see a problem with functional approach as in React. I do see a problem with understanding of how UIState being managed between framework, ui library and rendering engine.
Functional doesn't mean stateless. I'd argue functional programming is superior for representing state in user interfaces. For a success implementation, see Jane Street's Bonsai:
> functional programming is superior for representing state in user interfaces
It's always going to be slower than using something imperative. Trying to process the entire world's state for the sake of purity can feel elegant but it isn't free. And the complexity that gets added to make things performant is worse than just accepting that UIs are going to require you to jump around the tree and modify state.
Every time I go through the trouble of understanding the latest web technique (React, Elm, Signals (the newest solution), etc.) to deal with state management and the DOM, I end up walking away disappointed. There's nothing new in them that you can use to improve what we've been doing for ages in native GUI toolkits.
And to jump back to the original topic, yes, Cocoa was pretty decent, and SwiftUI while nice in many ways tries to Reactify native macOS development and made it worse. And it made Swift incredibly more complex and worse in hindsight.
The goal of the reactive/declarative approach was never to be more performant than imperative code. The goal is to more easily build UI that is performant enough and functions correctly. With imperative UI code it is incredibly easy to forget an edge case in your update logic.
MVC is not bad, but it is not a silver bullet. Calling MVC an ultimate solution to UI is oversimplification. Just looking at the steps you listed I can ask:
How do you collect all notifications on step 2 to fire them on step 3 such that UI does not re-render itself too much? E.g. updating a title of each item in a list of 100 items should not trigger 100 renders. Or 100 layout calculations (which I think is harder to avoid).
How do you deal with situations where on step 4 UI triggers an event that your model happens to listen and the cycle repeats while killing performance?
Because you rely on events how do you avoid “event hell”? That is, a situation when an event handler triggers a change that triggers another event handler that triggers a change and so on. Sometimes it is scrolling or typing, sometimes it is parts of the model subscribed to each other bubbling events to UI.
I never claimed MVC is a silver bullet. Just that it solves "... incredibly easy to forget an edge case in your update logic."
> UI does not re-render itself too much?
Glad you asked! In my Blackbird reference architecture (which is an instance of MVC), I use a coalescing queue to capture the updates. The coalescing is two-level: first, simple duplicates are weeded out. Second, if the update queue gets very full, it becomes coarser-grained, and weeds out duplicates based on that coarser grain. This has multiple steps of grain up to "just re-render the whole UI". Worked like magic in Wunderlist. Except it wasn't magic at all and very simple, inspectable and tractable.
> step 4 UI triggers an event that your model happens to listen
That's not allowed in MVC.
> Because you rely on events how do you avoid “event hell”?
I don't "rely" on events and there is no "event hell". Events are only used in the M→V communication part and there are no subsequent triggers, because the only event is "the model has changed", with an optional payload specifying which part of the model. Important: it must not contain the data that changed, this the view has to fetch from the model once it processes the update event.
Since the only event used is "the model changed", the view cannot ever be a source of those events, so no "event hell".
How do you handle UI state vs. underlying data (model) state, and dependencies between them? By UI state, I mean things like scrollbar position and selection state. When displaying a scrollable and selectable list of items, then for example when the number of items changes, the selection may need to adjust, and the scroll position may need to adjust. Depending on which items are added or removed (or reordered), the selection and scroll position may need to change differently for the apparent UI state to look stable for the user. If only the model is changed, a previous UI state like selection or scroll position may become invalid in relation to the new model state. Who updates the UI state accordingly to make it valid again? In the general case, application code needs to be involved in choosing the desired valid UI state when the underlying model state changes. How is the corresponding application code prevented from triggering further events?
When you have stateful view objects, these stateful view objects maintain the view state. When updating themselves with new data due to a ModelDidChange notification, they take care of reconciling their current display state with the underlying model state.
> When displaying a scrollable and selectable list of items
So for example an NSTableView or NSCollectionView. I personally use a subclass that interacts directly with a table representation, meaning a lot of the glue code that Cocoa(Touch) requires disappears.
> Who updates the UI state accordingly to make it valid again?
Always the view. Who else?
> In the general case, application code needs to be involved in choosing the desired valid UI state when the underlying model state changes.
How so? The view is always a reflection of the model data. Whether that is a "change" is actually mostly irrelevant, even though the notification is called ModelDidChange in my case. In Smalltalk MVC it is the #changed message. It means "you are out of date, please make yourself reflect the model".
This same mechanism also handles the model being changed by some other party without any further code. "The model has changed, please update yourself to reflect the current state of the model". That's it, modulo optimizations.
> How is the corresponding application code prevented from triggering further events?
Model code isn't involved. A ModelDidChange event is only triggered when...er...the model changes.
That said nothing prevents you from manually invoking the ModelDidChange notification, just like nothing prevents you from calling abort(), running an infinite loop, creating an unbounded recursion or reading from /dev/random until it is exhausted ...
Doing it by accident, though, is very hard, because it just isn't part of the programming model.
>The coalescing is two-level: first, simple duplicates are weeded out. Second, if the update queue gets very full, it becomes coarser-grained, and weeds out duplicates based on that coarser grain
This is not about duplicates. For example, sync updates 100 items in a list changing their titles. Items are bound to a list in the UI. Thus, 100 unique title update events triggered.
>Events are only used in the M→V communication
I don’t understand. Button clicked -> model change -> view update -> new event triggered -> model or view updated again … This is not something one would code on purpose, but often an attempt to create relationships between view. Like a custom layout code. Might not include model at all, just views being updated in an event handler trigger more events and more updates to views.
> For example, sync updates 100 items in a list changing their titles. Items are bound to a list in the UI. Thus, 100 unique title update events triggered.
Those "updates" go in the queue. When the UI gets around to updating itself, it looks at the queue and invalidates all the UI elements that refer to the model items in the queue.
It then updates those elements, using the coarsening to update larger elements in bulk if that becomes better.
> Button clicked -> model change -> view update -> new event triggered -> model or view updated again
Once again, that is not allowed. View updates are not allowed to trigger any events in MVC. A model → view update updates the view. That's it.
The only event is "model changed", so it also doesn't make sense for the view to generate those events.
I can only say how I did this in the Azul GUI framework[1] (note: not production ready yet), which may be close to what you're describing. So in Azul, you do this:
So, there's no "automatic" re-render, a callback has to return "Update.RefreshDom" or "Update.DoNothing" (default).
Now to your questions:
> How do you collect all notifications on step 2 to fire them on step 3 such that UI does not re-render itself too much?
Diffing, and then caching very aggressively. The click causes the model to re-call the layout() fn to return the entire DOM, however, there are ways to make this step very fast (arena allocation / no allocation). Then this gets diffed with the previous DOM state and the framework internally reuses everything it can (with user providing keys for list items, like React does).
> How do you deal with situations where on step 4 UI triggers an event that your model happens to listen and the cycle repeats while killing performance?
Azul has a "max recursion depth" of 5 and then just throws an error (infinite cycle). So, it will invoke all the relevant callbacks for a frame, then "sum up" all of the Update enums (i.e. one callback returned RefreshDom -> now we need to repaint).
> Sometimes it is scrolling or typing, sometimes it is parts of the model subscribed to each other bubbling events to UI.
Scrolling, selection, typing, etc. are handled by the framework. To make something editable, you need to set "contenteditable=true" on the Dom node (like on the web). Then, on text editing (which can also come from IME, a11y input, copy-paste), you get a "text changeset". The callback can then "reject" the changeset or allow it (default, since you already set contenteditable before).
Azul has a "dual update pattern" for performance here, i.e. the DOM itself is immutable until the next layout() call, however for "quick edits" like dragging a node you obviously don't want to call layout() again and construct an entire new DOM tree. So there, you just (conceptually, don't know the current API for this):
def on_div_dragged(data, info):
mouse = info.get_window_state().mouse_state
info.set_css_property(info.get_hit_node(), "transform: translate(%s, %s)", mouse_state.x, mouse_state.y)
# store in data model or node if necessary
data.user_mouse_pos = mouse_state
return Update.DoNothing # no re-render here
So, if another callback fires in between, the data model is still properly up to date. Azul also aggressively reconciles focus, scroll position, selection, text cursor position, etc. But Azul does not allow "one event auto-triggers another" like SolidJS does, it looks nice on a slide deck and then is a pain to debug Rube-Goldberg state machines.
This also works for text input or updating images (i.e. you don't need to call layout again on text input). Update.RefreshDom is for "larger / structural" changes, i.e. something like a route switch in a SPA-style app. Azul tracks the text cursor position by diffing the actual text, so the user code doesn't have to track the text cursor and state is preserved during a diff (it can also retain heavy elements).
For large lists, there is a native "virtualized view" DOM node with a callback that is being called "during" layout (after the size of the container has been determined, then the framework asks you to render your DOM, given the scroll position). So, that can be diffed, too. You never render in the DOM more than ends up on screen, so the perf is manageable.
Scrolling and retaining scroll positions inside a virtualized view is still an ongoing topic (not impossible, you just have to have functions to measure the DOM items before you return them, to estimate how much you need to render, and then do the math for "where are we right now, where is the scrollbar, how big is the virtualized view in relation to what we're rendering" - so the framework can set the right scrollbar size and position).
Again: please don't use or post Azul here on HN yet, docs are still slop and undergoing review, API is unstable until I have some apps going, but I just wanted to answer these questions.
Is the UI updating itself automatic or manual? Because if it’s manual, that’s precisely the error-prone part that you’re saying this approach somehow solves - you’ve done the “How to Draw an Owl” meme. If it’s automatic, that doesn’t seem especially different from the React/Redux/Elm/SwiftUI approach (as a sibling points out).
Yeah, M-V-C are all roles, not concrete objects. The C mediates between the input devices and the model, but in practice views can and often do fulfill that role as well. Cocoa views, for example, also fulfill the C role.
Different formulations of M-V-C have the C deal with more complex interactions, with sequences of interactive prompts like wizards.
The update is essentially automatic, and yes: MVC already solved the "problem with MVC" React/Redux/Elm/SwiftUI claim to solve. In 1979.
I like my Controller to be responsible for all the "business logic" so that its all in one place. It's the important part. The View layer is always fairly verbose and full of fluff. Especially if you have a lot of animation and formatting type code.
> I like my Controller to be responsible for all the "business logic" so that its all in one place.
Business logic is supposed to go in the model. All of it. Because it's the important part.
Controller these days can be largely empty.
"MODELS
Models represent knowledge. A model could be a single object (rather uninteresting), or it could be some structure of objects.
There should be a one-to-one correspondence between the model and its parts on the one hand, and the represented world as perceived by the owner of the model on the other hand. The nodes of a model should therefore represent an identifiable part of the problem.
The nodes of a model should all be on the same problem level, it is confusing and considered bad form to mix problem-oriented nodes (e.g. calendar appointments) with implementation details (e.g. paragraphs)."
Conceptually, the UI re-renders itself completely in order to always be an accurate reflection of the model.
That is the #1 job of the view: be an accurate reflection of the model.
And re-rendering itself completely is a safe way to implement that requirement.
However, the UI can also look at the model in more detail and figure out what parts need to change, as long as the effect is the same as re-rendering everything.
And the model can tell the view that specific subparts of the model have changed to make that job easier for the view.
But if it can't figure out the details, the fallback is to re-render the entire view from the model. But not to recreate the view. The view sticks around.
One way of doing this optimization is "damage rects" like Cocoa does. Another are the polymorphic identifiers used in the update queue of Blackbird.
MVC, the controller is the intermediary between the services/data models, and the views.
That still one of the best / simplest way to build large apps. MMVC is just a variation of it, with the models being able to communicate state to views and bypass controller if needed.
MVC, is still one of those 'fundemental as simple as it gets, and it gets the job done' patterns.
User interfaces representation are mostly trees. And with functional programming you basically have Tree2 = f(Tree1). Until f is done you can't do anything really. React has a lot of escape hatches to improve performance, but they are escape hatches, not an endorsement of the architecture.
With imperative programming (and OOP), you only have that single `Tree`, which you update at will. Less elegant yes, but we have modularization to help us there. What Emacs does is to keep that `Tree` as a single mutable object, but have the code be functional, while the results are imperative.
Imo the biggest issue with this functional model (at least in React), is that it handles things like virtualization, async, etc. poorly. Which is kinda ironic, because in a true functional language, it'd be feasible to provide 'a world model' - that is act as if the entire state is always available, and let the engine decide when to evaluate pieces of code - without any effort from the part of the programmer.
Unfortunately when complex state transitions, async, and virtualization enters the discussion, the magic of React breaks, and you have to deal with all that, and also deal with how React's engine handles things under the hood.
Stuff like virtualization (if we're talking about stuff like virtualized lists) is hard not because of React, but because there just isn't any support for it in browsers. React doesn't really help here, but in my experience, it's usually the browser that starts choking on high element counts, not React.
Async is just difficult in general though. It's not really a surprise that most libraries/frameworks converged on similar designs.
I am talking about virtualized lists. And it should be a framework feature. I used to use WPF on desktop, and it had pretty good virtualization support (though the framework in general was more like Angular) - most containers had virtualization support, and you only had to implement the logic on the data source, and they framework created and managed physical UI elements for you, and managed the mapping so it seemed seamless to the user.
React also operates on a virtual dom, there's no reason imo why couldn't they just fake that for you.
I mean, it's not quite that easy. The web is a lot more dynamic than WPF. If you just want virtualized homogeneous lists, there are libraries for that (and grids).
But, once you start hitting things like differently sized elements, search and so on, you start running into platform limitations that you won't be able to resolve in React land.
I know, that's why I said, that when you hit things like that (state that is too big to pull onto the client at once, and/or displayed at once), React's (and I guess a lot of other immutablity-based frameworks') dataflow management stops being magic, and you have to start tending to it.
Which usually means this model loses all advantages compared to simple MVC, or imperative systems, and at worst, becomes another headache as implementation details start leaking.
I guess my point was that it doesn't matter whether your framework/library is immutable/mutable/retained/functional/MVC/MVVM or whatever. You're hitting platform limitations one way or another.
But the rest of your app still gets the simplicity of a declarative programming model.
Imo that's a rather nihilistic take. My personal opinion is that despite ungodly amount of money invested, the web has changed very little from the 'static website +maybe jQuery' days - 90% of the content people consume is static, with very little interactivity, that would be perfectly captured by the 'script' nature of Javascript (think 10 line scripts, like 'if hovered, play live preview of video'). Even that interactivity is mostly driven by data that needs to be fetched from the server.
'Web Apps' are the exception, and are more or less completely alien to the rest of the content, think 'flash games' or 'google maps' - which have become 'wasm games on unity on itch.io'.
Having a mostly static web for the former, and the ability to basically almost whatever would be for the best.
Speaking of Flash, the 'cross section' of the two - websites with tons of interactivity and flashy animation have almost disappeared.
The web of today is an unhappy compromise of mediocrity and low expectations.
React works well, because it's only an abstraction over the real DOM. React only handles your app state. But the DOM mechanism is still very performant and very much imperative. But I don't think something like React would work well in a mobile app, because the UI tree is often very simple on iOS and macOS.