-
Notifications
You must be signed in to change notification settings - Fork 22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
First draft of Xilem blog post #71
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very interesting read overall but a few things came to mind while reading it:
- I think the part detailing the id path stuff could do with a sentence or two explaining why this is preferable to the global state approach you mention.
- The section on event propgation and mutable access to the app state made no sense to me personally. I will check the prototype code on this but I think the explanation could do with being expanded with some more example code.
- Possibly same for the adapt and memoize nodes but if it's just a high level overview you're aiming for then those sections are fine.
- I would be interested to know how this system deals with 'local state' that might be needed by a subtree of widgets but isn't derived from the app state. Is there a special node for this, or something else?
Maybe of-topic for this post but I would be interested to hear your thoughts on state management at some point. When comparing to state management in the elm architecture you stated that:
Some people like the explicitness of this approach, but it is unquestionably more verbose than a single callback that manipulates state directly as in React or SwiftUI.
It's my understanding that having app state centralized and updated with events actually makes it easier to scale an application, which is why Redux is so popular in React. I can infer that you have a different opinion on that but I would be interested to hear what your thoughts are on the scalability of Xilem's approach.
_posts/2022-04-24-ui-architecture.md
Outdated
|
||
## Synchronized trees | ||
|
||
In each "cycle," the app produces a view treeRendering in Xilem begins with the view tree. This tree has fairly short lifetime; each time the UI is updated, a new tree is generated. From this, a widget tree is built (or rebuilt), and the view tree is retained only long enough to assist in event dispatching and then be diffed against the next version, at which point it is dropped. In addition to these two trees, there is a third tree containing *view state,* which persists across cycles. (The view state serves a very similar function as React hooks) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Missing period and space after 'view tree'.
- Might be worth clariying that the widget tree persists across cycles.
- Does the view state have to be a tree? Or could it be a graph? I'm thinking of derived data from Recoil if you're familiar.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The view state is stored as a tree, yes. I'm not familiar with Recoil but I certainly am aware of more general incremental computation engines (Salsa, Adapton, Incremental, etc) in which the dependencies are explicitly modeled as a graph.
This is all potentially a much deeper conversation. I believe, without having gone too deep into implementation, that it would be fairly straightforward to wire up an actual graph incremental engine, and have the view state tree consist of lightweight references into that engine. (such an engine could be made available through context/environment, which is not currently part of the implementation but is planned)
So basically this is an area where I want to see what happens.
_posts/2022-04-24-ui-architecture.md
Outdated
|
||
In each "cycle," the app produces a view treeRendering in Xilem begins with the view tree. This tree has fairly short lifetime; each time the UI is updated, a new tree is generated. From this, a widget tree is built (or rebuilt), and the view tree is retained only long enough to assist in event dispatching and then be diffed against the next version, at which point it is dropped. In addition to these two trees, there is a third tree containing *view state,* which persists across cycles. (The view state serves a very similar function as React hooks) | ||
|
||
Of existing UI architectures, the view tree most strongly resembles that of SwiftUI - nodes are plain value objects. They also contain callbacks, for example specifying the action to be taken on clicking a button. Like SwiftUI, but somewhat unusually for UI in more dynamic languages, the view tree is statically typed, but with a typed-erased escape hatch (Swift's AnyView) for instances where strict static typing is too restrictive. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The term node
is first mentioned here and it's not clear if this refers to a view
or a widget
.
_posts/2022-04-24-ui-architecture.md
Outdated
|
||
## A worked example | ||
|
||
We'll use the classic counter as a running example. It's very simple but will give insight into how things work under the hood. For people who want to follow along with the code, check the idiopath directory of the idiopath branch; running `cargo doc --open` there will reveal a bunch of Rustdoc. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possibly this is obvious depending on the audience but it might be worth mentioning that this is a branch on the druid repo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also added a link. I'm slightly uncertain about all this as there's a chance I might rename the branch/directory with the "Xilem" branding, but I'll leave it as-is for now.
_posts/2022-04-24-ui-architecture.md
Outdated
|
||
This was carefully designed to be clean and simple. A few notes about this code, then we'll get in to what happens downstream to actually build and run the UI. | ||
|
||
This function is run whenever there are significant changes (more on that later). It takes the current app state (in this case a single number, but in general app state can be anything), and returns a view tree. The exact type of the view tree is not specified, rather it uses the [impl Trait] feature to simple assert that it's something that implments the View trait (parameterized on the type of the app state). The full type happens to be: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'simply' rather than 'simple'.
_posts/2022-04-24-ui-architecture.md
Outdated
+------+ +--------+ | ||
``` | ||
|
||
The idea of assigning a stable identity to a widget is quite standard in declarative UI (it's also present in basically all non-toy immediate mode GUI implementations), but Xilem adds a distinctive twist, the use of *id path* rather than a single id. The id path of a widget is the sequence of all ids on the path from the root to that widget in the widget tree. Thus, the id path of the button in the above is `[1, 3]`, while the label is `[1, 2]` and the stack is just `[1]`. The full id path is redundant if we had global information about the structure of the tree (for example, by following parent links), but the point is that given id paths, we don't *need* to track this kind of information. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this 'id path' preferable to global information about the tree structure? You've said that with the 'id path' you don't need that information but not why that is better. I could imagine a system where some context is passed to widgets on constrution which keeps track of this information and would be transparent to the user who interacts with just the views.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added something, but I think you've asked a deep question here, and I'm not positive I can give a good argument at the level of writing I'm doing.
I did indeed in an earlier prototype have tree structure tracking that was done on construction/mutation of the widget tree, so the is visible during reconciliation were just plain integer tokens (not paths). One of the subtle ways that breaks down is ids allocated to futures - they might not correspond to an actual node in the widget tree, or perhaps dummy nodes would be added. Same for env_get, which I've written about on Zulip but not in this blog.
Note also that Elm has an analogous problem; Html has map
but there is no comparable component-forming operation for subscriptions. As far as I know, those have to be addressed to the top-level update method of the app logic.
I'll think a little more about whether I can explain things better. Most often, when I find things hard to explain, it's because of a gap in my own understanding.
_posts/2022-04-24-ui-architecture.md
Outdated
|
||
After clicking the button and running the callback, the app state consists of the number 1, formerly 0. The app logic function is run, producing a new view tree, and this time the string value is "Count: 1" rather than "Count: 0". The challenge is then to update the widget tree with the new data. | ||
|
||
As is completely standard in declarative UI, it is done by diffing the old view tree against the new one, in this case calling the `rebuild` method on the `View` trait. This method compares the data, updates the associated widget if there are any changes, and also traverses into children. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does this diffing occur if the view tree is short-lived? Is there a copy of the view tree being stored accross cycles?
_posts/2022-04-24-ui-architecture.md
Outdated
|
||
It would be very limiting to have a single "app state" type throughout the application, and require all callbacks to express their state mutations in terms of that global type. So we won't do that. | ||
|
||
The main tool for stitching together components is the `Adapt` view node. This node is so named because it adapts between one app state type and another, using a closure that takes mutable access to the parent state, and calls into a child (through a "thunk") with a mutable reference to the child state. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You haven't defined what a 'component' is. Previous to this the reader knows only about views, widgets, and view state. Is a component just another term for a view? This section is titled 'components' but talks mostly about the adapt node (view?).
_posts/2022-04-24-ui-architecture.md
Outdated
|
||
The main tool for stitching together components is the `Adapt` view node. This node is so named because it adapts between one app state type and another, using a closure that takes mutable access to the parent state, and calls into a child (through a "thunk") with a mutable reference to the child state. | ||
|
||
In the simple case where the child component operates independently of the parent, the adapt node is a couple lines of code. It is also an attachment point for richer interactions - the closure can manipulate the parent state in any way it likes. The event handler of the child component is also allowed to return an arbitrary type (unit by default), for upward propagation of data. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand what any of this means. Maybe an example would help? Does this section require knowledge of Druid because I'm not sure what it means by 'event handler' and 'return an arbitrary type'.
|
||
Ron Minsky has [stated][Signals and Threads: Building a UI framework] "hidden inside of every UI framework is some kind of incrementalization framework." Xilem unapologetically contains at its core a lightweight change propagation engine, similar in scope to the attribute graph of SwiftUI, but highly specialized to the needs of UI, and in particular with a lightweight approach to *downward* propagation of dependencies, what in React would be stated as the flow of props into components. | ||
|
||
In this particular case, that incremental change propagation is best represented as a *memoization* node, yet another implementation of the View trait. A memoization node takes a data value (which supports both `Clone` and equality testing) and a closure which accepts that same data type. On rebuild, it compares the data value with the previous version, and only runs the closure if it has changed. The signature of this node is very similar to [Html.Lazy] in Elm. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previously you said that Xilem:
removes some of the limitations. In particular, Druid requires app state to be clonable and diffable, a stumbling block for many new users
But this seems to be re-introducing that limitation if the user wants this finer grained change propagation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. It's a tradeoff, and the difference is that it's now opt-in, while Druid absolutely required it.
_posts/2022-04-24-ui-architecture.md
Outdated
) | ||
``` | ||
|
||
This logic propagates the change up the tree *only if* the child state has actually changed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't you want change to propagate down the tree? Actually, which tree is this referring to?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The app state tree. But I'm going to change the way I word this, because there's no requirement that the app state be modeled as a tree, unlike original Druid. It's quite fine for the app state to be a graph (and we're going to have this in Runebender, as glyphs referencing components is definitely graph rather than tree structure). Good catch, thanks.
Thanks for the detailed commentary @geom3trik! I've been so close to these problems so long it's not easy to get calibrated on what I can take for granted and what needs to be explained in more detail. This will definitely help make the post better, and I hope to have another draft out soon. |
A fair amount of new writing in response to feedback, as well as visuals for the worked example. Hopefully things are clearer now.
Disclaimer: I'm a Web front end developer, and not familiar with native UI. Web frameworks have some interesting ideas. React's concurrent mode I heard that druid is switching to a tic model, which I think it is possible to scheduling. React works in concurrecy mode to prevent blocking.
Timer queue and task queue are separated, and when the time expires, it will be executed from the timer queue to the task queue. They increase responsiveness by giving priority to sync, input, general, and idle. Automatic batching is also possible when events and timeouts occur, which improves performance. More context
Vue's Compiler-Informed virtual dom & Glimmer VM Vue provides a way to compile components. Ember's glimmer generates a kind of bytecode. More context
Solid's reactivity & state Fine-grained reactivity can lead to minimal state changes. An example of a neat library is recoil. Another way to deal with a complex state without the reactive, is statecharts. (Famous library: xstate) More context
Mikado's pooling Mikado has a fairly unique pooling system. Style Freestyler caches styles according to cardinality, The most advanced aspect of the web in terms of style and theme is how it manages.
Others
|
Closes #70