fabulous-dev / Fabulous

Declarative UI framework for cross-platform mobile & desktop apps, using MVU and F# functional programming
https://fabulous.dev
Apache License 2.0
1.15k stars 122 forks source link

Fabulous with Incremental/Adaptive Views #258

Closed cloudRoutine closed 2 years ago

cloudRoutine commented 5 years ago

In "Data Driven UIs Incrementally" (Strangeloop 2018 talk) Yaron Minsky goes through how they built a diff+patch approach on top of functional data structures using incremental to structure the process of updating the model in the Elmish architecture as an incremental computation.

But I still think this falls short of what could potentially be achieved by integrating all of these pieces into a compositional primitive to make it a lot easier to build and extend reactive user interfaces in F#. An abstraction similar to Active Expressions should be possible, although the underlying implementation would be built on an incremental computation model.

If Fabulous was extended to support this kind of architecture maybe it would make sense to decouple that from Xamarin.Forms? Xamarin.Forms would still be the canonical implementation, but if someone wanted to plug in say Avalonia instead it would be amenable to it.

Further Reference

Blog Posts

Papers

Repos

TimLariviere commented 5 years ago

Wow, that looks really interesting! Thanks! I didn't know about Active Expressions. Time to study :)

Swapping Xamarin.Forms with another UI Framework (Avalon, Uno, native Android/iOS, etc.) is a recurring question. I think nothing prevents to decouple Xamarin.Forms and Fabulous, especially if we implement a diff+patch on the model instead of the view. (still need to learn how Active Expressions will help here) But the work to support another framework will still be substantial as I think the best way is to have a specific View DSL per framework.

lukethenuke commented 5 years ago

@cloudRoutine I only skimmed the links but have you taken a look at http://reedcopsey.github.io/Gjallarhorn/ ? :)

TimLariviere commented 5 years ago

Fabulous has been decoupled from Xamarin.Forms and is now able to provide its own optimized view diffing. These links will come in handy soon. :)

dsyme commented 5 years ago

Watching through Minsky's excellent talk, I notice the following:

First, from about 25min in, I see F# immutable maps should support a "symmetric diff" operation with signature such as

val symmetricDiff: Map<'K,'V> -> Map<'K,'V> -> ('V -> 'V -> bool) -> seq<'K * Choice<'V, 'V, ('V * 'V)>>

It is necessary to implement this on the core tree of the Map data structure to allow pointer-equality on the internal Map structure to be used as a way of detecting "no change" within incrementally derived maps.

Next, I wonder if there is scope to experiment with a quotation-based "incrementalization" of Fabulous programs. My thinking is that we put ReflectedDefinition on all the MVU code, and then

mkIncrementalProgram <@ init @> <@ update @> <@ view @>

does a somewhat sophisticated derivation of an incrementalized version of the MVU program. This would include an incrementalized model type under the hood (not made explicit to the programmer). For example, if the model contains

type Model = { Value : int }

and the update function (unsurprisingly) does incremental updates to Value for some messages, then the incrementalized program uses a model

type Model' = { Value: Incremental<int> }

in either a Gjallahorn or Self-adjusting computation version of incrementalized or reactive computation. The update function would be rewritten to be Model' -> Model' and the view function would be rewritten to be an incrementalized view Model' -> dispatch -> Incremental<View'>. The process of consuming the incremental view would then be ultra-fast, and would not involve re-evaluating the whole view.

The Model' type would not be explicit to the user and might not actually be generated as a class (it would exist logically in the implementation of mkIncrementalProgram but not otherwise).

The approach might be extended to allow the use of any data structures in the model that support "diff" and "patch" operations with respect to an underlying incremental-friendly representation, and the ability to correlate those with the actual canonical update code.

This would be a bit of a research agenda but feels somewhat tractable - it is effectively rediscovering the Xaml/Gjallahorn-style binding structure from the static structure of the code where possible, and would resort to dynamic view re-execution where not possible. An advantage over the OCaml-style code presented by Minsky is that the user would not have to program explicitly using Incremental nor Rx-style combinators but can just program in the naive functional programming way. One disadvantage is that the user may not understand which parts of the code are executing incrementally and which are executing using view re-evaluation.

davidglassborow commented 5 years ago

As a reference, a port of incremental to F# - https://github.com/isaksky/Incremental.NET

dsyme commented 5 years ago

@davidglassborow Thank you for that link!

dsyme commented 4 years ago

A quick update: we are working on an incremental/adaptive data library for F#, based on the core of the Aardvark adaptive data implementation, which is tried and tested

https://github.com/fsprojects/FSharp.Data.Adaptive

Early days yet, but I'd like to next experiment next with an adaptive-data version of Fabulous ViewElements. I'll post here if/when I make progress.

See also https://github.com/krauthaufen/aardvark.web/blob/master/src/Aardvark.UI.Web/DomNode.fs#L13 for an adaptive DOM node type

See also https://github.com/aardworx/aardvark.web/blob/master/src/Aardvark.UI.Web/Ui.fs for the web of reader/updater objects that sit on top of the adaptive data and propagate changes to the DOM

JaggerJo commented 4 years ago

I think FuncUI (vNext) ticks a lot of the boxes from above.

dsyme commented 4 years ago

@JaggerJo The FuncUI implementation is really good - this file is impressive https://github.com/dsyme/Avalonia.FuncUI/blob/master/src/Avalonia.FuncUI/Core/VirtualDom.fs

That said, it's still using view-reevaluation - for example if there are 10K data points in a chart and one is removed then the view is re-evaluated and, in the absence of other hacks, this will involve a significant amount of work to spot the minimal diff.

FuncUI can, I think, be adapted to work with adaptive data relatively easily. This would mean that the 'view' functions are not re-executed on update (except where necessary for incremental DOM maintenance). The diffs in the view would flow out of the adaptive data, rather than having to diff an old and new view like you do here

There's a sample showing how to define a tree of adaptive view-like data and perform incremental maintenance on a mutable HTMLElement data strucutre here: https://github.com/dsyme/FSharp.Data.Adaptive/blob/dom-node/src/FSharp.Data.Adaptive.Tests/DomUpdater.fs. The variation FuncUI would need would be a bit different - for example FuncUI could define a ViewReader for the AdaptiveView type, producing a ViewDelta (and then patch is called on that).

krauthaufen commented 4 years ago

Hey all, I'm bascially done with FSharp.Data.Adaptive for the time being and currently focus on a better HashMap implementation (see ImmutableHashCollections).

I'd be happy to help with sketching adaptive-support for Fabulous but I have 0 experience with it yet. Just let me know if you're interested and I'll try to come up with a sketch. Maybe I/we should start with a very restricted set of elements without the auto-generated code... Cheers

dsyme commented 4 years ago

@krauthaufen Yes, I'm sort of skirting around biting this off too.

One reason why I did the DOMNode sample in F.D.A was to start to get a feel for what an AdaptiveViewElement type would look like in Fabulous. Certainly the code generator would need to be modified as well, which in final form should be done after #530 lands though of course we could experiment earlier.

Currently attending NDC Sydney so a bit restricted in time - need to prep my talks :)

TimLariviere commented 4 years ago

@krauthaufen @dsyme This looks fantastic. I will definitely play with it. πŸ˜„

I have only a basic understanding of it, and I have a few questions regarding what it would mean for Fabulous.

Will it be something used only internally while keeping the usual init/update/view loop Or would the users need to use aval, amap, etc. constructs too?

Will it be also applicable to state (model)? (by auto-magically creating an incremental model like you mentioned in a comment above)

And finally, is it something that could fit directly in Fabulous itself, removing the need for implementing a view diff process in Fabulous.XamarinForms?

dsyme commented 4 years ago

@TimLariviere I think the aim would be to give an advanced option for people who are dealing with rapid UI updates or large view computations, and wehre using techniques like dependsOn don't cut it.

Using adaptive data is fairly invasive in the programming of the view - but "transitional" in the sense that it's a fairly syntactic process to switch from view-recomputation to adaptive data. It is also somewhat invasive on the programming of the model. However the result is minimal, tight updates

An example/sketch extracted from the unreleased Aardvark.Web is here:

The crucial thing is really that the Adaptive view is authored in an essentially functional way, MVU-style, using the functional mindset, and is presumably derived from a slower adaptive view. However it is actually only computed once, or only recomputed on-demand when necessary in response to changes. For example, if the view has

adaptive { 
    let! loggedIn = model.LoggedIn
    if loggedIn then ... 
        ...
    else
        ...
}

then the conditional will only be re-executed if either model.LoggedIn actually changes or some adaptive value on which the currently active if/then/else branch changes. So the view is properly incrementalized - the view function is called once and the thing it returns acts like adaptive functional data.

Because of the use of adaptive { .. } and comprehensions over adaptive collections the view is written slightly differently, but it is not a vast change.

dsyme commented 4 years ago

To answer the questions:

Will it be something used only internally while keeping the usual init/update/view loop Or would the users need to use aval, amap, etc. constructs too?

See above, the view authoring changes. Some kind of auto-incrementalization based on quotation meta-programming over the view function may be possible

Will it be also applicable to state (model)? (by auto-magically creating an incremental model like you mentioned in a comment above)

See Adaptify above for how Aardvark does this. You could also work with an adaptive model in which case you're really in adaptive end-to-end and the whole thing begins to feel more like mutable programming, ala Gjallahorn or WebSharper.UI (both great, but the use of an immutable model gives a midpoint on the spectrum)

And finally, is it something that could fit directly in Fabulous itself, removing the need for implementing a view diff process in Fabulous.XamarinForms?

I think the code generators for the ViewElement constructors will need to generate code to do adaptive diffing. This is the part that is still a bit of a mystery too me but I think I understand how it would go. Each View.Button would need an AView.Button counterpart and these would be used once you decided to switch to adaptive views. ViewElement and AdaptiveViewElement would be different types. The code generator would presumably generate both View.Button and AView.Button.

krauthaufen commented 4 years ago

@dsyme cool overview. The auto-incrementalization of view functions could be one of the long-term goals here, but the current way of directly writing adaptive view functions also has its benefits, since it allows users to control how updates will be performed.

I could dig into xamarin forms a little and come up with a prototypical implementation for some of the components. This way I learn a little about xamarin.forms and you guys get an idea of how this could look like.

Cool to see that there's some interest in our baby 😁

Cheers

krauthaufen commented 4 years ago

Hey, I wrote a little sketch playing around with Xamarin testing ways to interact with FSharp.Data.Adaptive. (see XamarinAdaptiveSketch)

This does not include any ELM-style updates, but focuses on getting adaptive values into the DOM.
Please note that this is a super-hacky and ugly code currently but I managed to create a counter-app.

Some things I'd like to mention

dsyme commented 4 years ago

I've made fabulous-adaptive - a proof-of-concept investigation of using FSharp.Data.Adaptive for incremental view update in Fabulous. There's also a version applying it to Fable/Elmish floating around (cc @krauthaufen)

My aim in this experiment has been to make a fork of Fabulous (e.g. "AdaptivelyFabulous") with as few changes to the Fabulous programming model I've been able to achieve.

The diff is here. There are numerous TODOs but the CounterApp is working.

My conclusions so far:

  1. It is possible. The fork is an incrementalizing version of Fabulous that is recognizably MVU programming in a style that all Fabulous programmers would recognize and be able to transition to if performance required it

  2. The programming model is impacted in small but not-insignificant ways.

  3. The implementation changes are fairly invasive, as the process of incremental update for ViewElement is changed.

I would expect the performance of fully completed AdaptivelyFabulous to be much, much better on data-rich views (e.g. very large collections/lists) but have yet to prove if this is the case.

I don't think it's yet feasible to make Fabulous use adaptive data by default, but it may be feasible to maintain it as a fork for advanced high-performance, data-rich view scenarios. It's also difficult to incorporate adaptivity as an option though this needs more investigation (I originally started down the path of adding AdaptiveViewElement in addition to ViewElement but abandoned this in order to just use adaptivity everywhere)

The programming model is affected in two major ways.

  1. The view must be written as an adaptive expression, example here. This basically means that the following changes are made from a normal Fabulous view:

    • invariant, unchanging attributes are prefixed with c
    • invariant lists of children prefixed with cs
    • varying attributes are written using AVal.map from an underlying source
  2. The functional model must be incrementalized, which means taking two models and producing changes via a Changeable model. This is currently done either by hand or by using a meta-programming solution such as the Adaptify tool or some future F# type provider.

    (Aside: Ideally this wouldn't have to happen at all and somehow the update function could just produce a list of deltas to the model which are then propagated through the incremental computation. I'll talk to @krauthaufen more about this, it's a deep issue that goes to the heart of the interaction between domain modelling (representing model state) and event sourcing (representing deltas to model state and processing them to produce updated models))

  3. The extension API is altered. I'm vaguely optimistic that we can find an extension API that combines the best of both worlds.

krauthaufen commented 4 years ago

Awesome work !!!! πŸ₯³πŸŽ‰

Ideally we could incrementalize pure view-functions using a compiler, but "chasing" updates through the update-function also seems promising. We thought about that in the past but never really did it...

However the Adaptify approach isn't too different from that actually. It computes changes on persistent structures that are expected to share most of their values. Nonetheless "knowing" which elements changed would of course be beneficial.

krauthaufen commented 4 years ago

Maybe we could come up with something similar to Fable.Elmish.Adaptive where users can mix and match adaptive and static components. This would certainly simplify transitioning from pure-world to adaptive-world...

However the high-level code would need to switch from a re-executing view function to an adaptive one, but the "standard" behaviour can always be simulated by using model.current |> AVal.map pureView...

dsyme commented 4 years ago

Maybe we could come up with something similar to Fable.Elmish.Adaptive where users can mix and match adaptive and static components.

Yes, this would be ideal. Right now the duplication/divergence in the update logic and codegen is too large and subtle for my liking to inflict on @TimLariviere and the others managing Fabulous - too many things would need to be done twice. I need to keep working on it.

dsyme commented 4 years ago

I've placed the adaptive version of Fabulous at https://github.com/dsyme/fabulous-adaptive. There is a small sample of its use in the README https://github.com/dsyme/fabulous-adaptive#fabulous-adaptive-version

I'll continue to maintain this as a fork with frequent merges from Fabulous master. There are a substantial number of TODOs (attached properties being the most important)

TimLariviere commented 2 years ago

Closing stalled issue