Comment by d12frosted

25 days ago

Good points, thanks for engaging thoughtfully.

On vui.el's approach - yes, the blessing is that widget.el is simple enough to build on. It does the "rendering" and some "behaviour", vui.el handles the rest.

On ECS vs OO - I'll admit I don't have enough experience to speak about UI paradigms in general. But my critique of widget.el is that inheritance hierarchies don't compose well when you need orthogonal behaviors. Composition feels more natural to me - could be just how my brain works, but it scales better in my experience.

On state management being independent - I'd be curious to hear more. Pathom is interesting for data-driven architectures. vui.el's state is intentionally minimal and Emacs-native, but you're right it could potentially be decoupled further.

On "why not full reactive" - to clarify what vui.el has: React-style hooks with explicit dependency tracking (vui-use-effect, vui-use-memo, etc.), state changes trigger re-renders, batching for multiple updates. What it doesn't have: automatic dependency inference or fine-grained reactivity where only specific widgets update. The tradeoff was debuggability - explicit deps are easier to trace than magic. But I'm open to being wrong here. What would you want from a reactive layer?

- "don't compose well when you need orthogonal behavior" ah okay, you've actually hit this case. I guess I haven't done anything gnarly enough to encounter this

> Pathom is interesting for data-driven architectures. vui.el's state is intentionally minimal and Emacs-native, but you're right it could potentially be decoupled further.

I'll be honest, I haven't yet written a Pathom-backed GUI. But I'm hoping to experiment with this in the coming weeks :)) cljfx is structured in such a way that you can either use the provided subscription system or you can roll your own.

> What it doesn't have: automatic dependency inference or fine-grained reactivity where only specific widgets update

So all the derived states are recalculated? Probably in the 95% case this i fine

In the big picture I enjoyed the cljfx subscription system so much, that I'd like to use a "reactive layer" at the REPL and in general applications. You update some input and only the parts that are relevant get updated. With a subscription-style system the downside is that the code is effectively "instrumented" with subscription calls to the state. You aren't left with easily testable function calls and it's a bit uglier.

Pathom kind of solves this and introduces several awesome additional features. Now your "resolvers" can behave like dumb functions that take a map-of-input and return a map-of-output. They're nicer to play with at the REP and are more idiomatic Clojure. On top of that your code turns in to pipelines that can be injected in to at any point (so the API becomes a lot more flexible). And finally, the resolvers can auto parallelized as the engine can see which parts of the dependency graph (for the derived state you're prompting) can be run in parallel.

The downsides are mostly related to caching of results. You need an "engine" that has to run all the time to find "given my inputs, how do I construct the derived state the user wants". In theory these can be cached, but the cache is easily invalidated. You add a key on the input, and the engine has to rerun everything (maybe this can be bypassed somehow?). You also can concoct complex scenarios where the caching of the derived states is non-trivial. Derived state values are cached by the resolvers themselved, but they have a limited view of how often and where they're needed. If two derived states use one intermediary resolver but with different inputs, you need to find a way to adjust the cache size.. Unclear to me how to do this tbh

  • Thanks for the detailed breakdown on Pathom and cljfx subscriptions - this is exactly the kind of perspective I was hoping to hear.

    The resolver model you describe (dumb functions, map-in → map-out, parallelizable) is appealing. It's similar to what I find elegant about React's model too - components as pure functions of props/state. The difference is where the "smarts" live: in the dependency graph engine vs in the reconciliation/diffing layer.

    Your point about the 95% case resonates with vui.el's approach. We do have vui-use-memo for explicit memoization — so expensive computations can be cached with declared dependencies. It's the middle ground: you opt-in to memoization where it matters, rather than having an engine track everything automatically.

    For typical Emacs UIs (settings panels, todo lists, file browsers), re-rendering the component tree on state change is fast enough that you rarely need it. But when you do — large derived data, expensive transformations — vui-use-memo is there. The tradeoff is explicit deps vs automatic tracking: you tell it what to cache and when to invalidate, rather than the framework inferring it.

    That said, I'm planning to build a more complex UI for https://github.com/d12frosted/vulpea (my note-taking library) - browsing/filtering/viewing notes with potentially large datasets. That'll be a real test of whether my performance claims hold up against reality. So ff vui.el ever needs to go there, the component model doesn't preclude adding finer-grained updates later. The should-update hook already lets you short-circuit re-renders, and memoization could be added at the vnode level.

    The caching/invalidation complexity you mention is what made me hesitant to start there. "Explicit deps are easier to trace than magic" was the tradeoff I consciously made. But I'm genuinely curious - if you do experiment with Pathom-backed GUI, I'd love to hear how it goes. Especially around the cache invalidation edge cases you mentioned.

    • I wrote my last message and then thought "oh gosh, it's so long, no one will read it". But I'm glad to find someone thinking about similar problems :))

      > It's the middle ground: you opt-in to memoization where it matters, rather than having an engine track everything automatically.

      What is the downside of just memoizing everything?

      > you tell it what to cache and when to invalidate

      I'd be curious to here more on this. I think the conceptual problem I'm hitting is more that these derived state systems (and this goes for a Pathom engine or a subscription model) seem to work optimally on "shallow" derived states. Ex: You have a "username" you fetch his avatar (one derived state) and then render it (maybe another derived state). That's fine.

      But if now have a list of users... each has usernames and derived avatars and rendered images.. the list of users is changing with people being added and removed - this get mess.

      With Pathom you can make username,avatar,render revolvers for the derived states. It's nicely decoupled and you can even put it in a separate library. With cljfx subscribers you can make subscriptions based on the state + username. It's more coupled, but you nolonger need an engine. Functionally it's the same.

      But when it comes to the cache, I don't actually know any system that would handle this "correctly" - clearing cache entries when users are removed. Under the current solutions you seem to only have two solutions:

      - You just sort of guess and make a "large enough" FIFO cache. You either eat memory or thrash the cache.

      - Make Resolvers/Subscriptions on whole lists. So now you have usernames->avatars->renderings. This makes the derived states "shallow" again. If any entry is changed, the whole lists of derived states are recalculated. Memory optimal and fast to fetch (don't need to look check the memoization cache a ton). But if your list is rapidly changing, then you're doing a ton of recalculation.

      I actually haven't worked with React directly, so I'd be curious to know how they propose dealing with this.

      > if you do experiment with Pathom-backed GUI, I'd love to hear how it goes

      Will do :)