Skip to content
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

Explore addition of a Virtual/Incremental DOM mechanism #13

Open
OlivierBlanvillain opened this issue Oct 31, 2016 · 21 comments
Open

Explore addition of a Virtual/Incremental DOM mechanism #13

OlivierBlanvillain opened this issue Oct 31, 2016 · 21 comments

Comments

@OlivierBlanvillain
Copy link
Owner

OlivierBlanvillain commented Oct 31, 2016

Especially needed to deal with mutable Seqs. Here is an abstract for the project:

Functional reactive programming (FRP) is a popular paradigm for developing web interfaces, but care needs to be taken in the DOM binding mechanism in order to have good performance.

State of the art DOM binding libraries such as Facebook React relie on (virtual) DOM diffing mechanism to obtain best performances. On every update, a complete view of the application is generated in memory. The framework then compute a diff between current and previous view to finally apply the minimum changes to the DOM.

Another approach, called precise binding, makes use of FRP constructs to build views while maintaining a dependency graph of Rxs, values susceptible to changes over time. This mechanism, currently using in the monadic-html library, allows changes to be propagate very precisely to the DOM. However, depending on the granularity of mutations, idiomatic monadic-html can be inefficient when re-rendering large portions of the DOM for small changes.

The goal of this project is to design a DOM diffing algorithm in Scala.js which could be combined with the existing precise binding mechanism to obtain an hybrid approach. Depending on time, the project would include benchmarks to compare this hybrid binding approach with existing solutions.

@OlivierBlanvillain
Copy link
Owner Author

OlivierBlanvillain commented Dec 15, 2016

@nartamonov
Copy link

nartamonov commented Dec 28, 2016

If I understand correctly, Binding.scala resolves this issue with observable sequence (Vars), so when some elements are updated/removed/inserted, library knows exactly what part of DOM should be mutated. monadic-html only have observable cell (Var) which can contain sequence, but when sequence are mutated library should rerender whole DOM tree for that sequence. Why can't we introduce observable sequence like in Binding.scala? Sorry if question is silly, I'm really novice in reactive programming.

@OlivierBlanvillain
Copy link
Owner Author

OlivierBlanvillain commented Dec 28, 2016

If I understand correctly...

You'r spot on!

Why can't we introduce observable sequence like in Binding.scala?

We could, but I would personally like to implement the more general solution of tree diffing. Let me explain what I dislike about a sequence only solution:

  • The API to manipulate Vars is imperative, in a way, it's exposing the diff API to the user.

  • It does not solve the case where you update one field of a case class Foo(s1: String, s2: String, ..., s22: String), you have to rerender a fooView. You could of have case class Foo(s1: Var[String], s2: Var[String], ..., s22: Var[String]), but that does not looks idiomatic to me, and ideally a pure performance optimization should not affect the current nice immutable code I have.

  • Similarly, if you are rendering large trees, maps or tuples, you have to fall back to the less appropriate abstraction of sequences to get nice performances.

@nartamonov
Copy link

Thanks, @OlivierBlanvillain! I understand now difficulties with Vars 😌

@bbarker
Copy link
Contributor

bbarker commented Mar 22, 2017

Would it make sense at all to make a separate library that does patch directly on scala.xml.Node? Such a project might be useful elsewhere, and could be used by mhtml. Of course, there are many subtleties (or maybe obvious points) I haven't grasped yet. Possible example: /~https://github.com/andyglow/scala-xml-diff

@OlivierBlanvillain
Copy link
Owner Author

OlivierBlanvillain commented Mar 23, 2017

@bbarker I think that could work, here are a few things to take into consideration:

  • mhtml currently uses a custom version of scala.xml, which means we should either rollback to /~https://github.com/scala/scala-xml now that is cross compiles to scala.js (and lose some type safety), cross compile the xml diff library against mhtml fork of scala.xml, or simply add more stuff directly in mhtml. The latter option might be more pleasant given that it gives the implementer full control over the AST.

  • Half of the work of a dom diffing algorithm is to apply patches to the actual dom. This will likely be tightly coupled with the way notes are mounted to the dom, so decoupling in diffing might increase the friction for patching.

@fdietze
Copy link

fdietze commented Mar 27, 2017

MetaRx solves the problem with delta objects:
http://metastack.pl/metarx/latest.html#buffers

@OlivierBlanvillain
Copy link
Owner Author

OlivierBlanvillain commented Mar 27, 2017

@fdietze This looks like the same solution that what's used in Binding.scala. As explained in this previous comment, I would rather go for a solution that does not require users to write in an imperative style.

Let me give an example of why I dislike the delta objects approach. Suppose I want to implement this page: https://en.lichess.org/games. I already have all my business logic implemented in term of the following:

case class Chessboard(...)          // Something that's easy to display on the screen
case class Move(from: Pos, to: Pos) // Any move on the board

def applyMove(m: Move, c: Chessboard): Chessboard =
  ??? // All the game logic goes here

def renderBoard(c: Chessboard): scala.xml.Node =
  ??? // All the front-end work goes here

If I want to animate the boards on the /games pages in a reactive way, I would have to turn my Chessboard data structure into a delta objects and, most importantly, rewrite all the logic in applyMove in a mutable/imperative style.

With React style dom diffing, I can keep all the existing logic (and the super simple renderBoard rendering function), and still count on the diff algorithm to save the browser from re-rendering every chessboard on every move.

@fdietze
Copy link

fdietze commented Mar 27, 2017

Ok, I understand. So maybe a hybrid approach would make sense. Delta updates only for common data-structures (like Lists) and Virtual DOM diffs for custom ones.

@OlivierBlanvillain
Copy link
Owner Author

Everything in the library was built to encourage immutability / referential transparency, I don't think exposing a mutable API for a few data-structures would fit well with what's already there...

Also, with a diff algorithm in place, why would you ever want to formulate your business logic in term of patches instead of functions over immutable data structures?

@fdietze
Copy link

fdietze commented Mar 27, 2017

I think the delta-approach doesn't necessarily need to be implemented with mutable data structures. The whole point are efficient updates which is also achievable with immutable datastructures. This should preserve the referential transparency property. Am I overlooking something?

I agree that you probably never want to formulate patches in your business logic. That's why some common data structures like lists should do that automatically. The usage would only shift from manipulating the immutable list manually to using methods to manipulate the reactive immutable list (and automatically handling the diff-part). Does that make sense?

@OlivierBlanvillain
Copy link
Owner Author

I think the delta-approach doesn't necessarily need to be implemented with mutable data structures

I have the impression that this is not the way it works in other libraries implementing delta objects, for example looking at the js-framework-benchmark for Binding.scala none of this is referentially transparent (they are mutations happening under the hood).

But if you have ideas for a different design I'm all yours!

@Atry
Copy link

Atry commented Mar 28, 2017

Binding.scala users usually use normal case class as data source and still get the benefits of partial updating.

They usually performs coarse-grained source updating, which triggers fine-grained dom updating. For example: /~https://github.com/walfie/gbf-raidfinder/blob/master/client/src/main/scala/walfie/gbf/raidfinder/client/ViewModel.scala#L72

@OlivierBlanvillain
Copy link
Owner Author

OlivierBlanvillain commented Mar 28, 2017

This is exactly what I mean by imperative style. In a way, Binding.scala imposes the use of mutable data structures (to get good performances). In the Chessboard example, it might be unrealistic to require a rewrite from

// Pure function
def applyMove(m: Move, c: Chessboard): Chessboard

to

// Updates the Var in c: Chessboard
def applyMove(m: Move, c: Chessboard): Unit

Diff/patch algorithms solves this, you can use immutable data structures and pure functions while still getting the benefits of minimal DOM updates.

@Atry
Copy link

Atry commented Mar 28, 2017

@OlivierBlanvillain That's why the library name is Binding.scala 😄

@Atry
Copy link

Atry commented Mar 28, 2017

I think FRP is a good tool for modelling state machine. On the other hand, data-binding is better for UI.

@OlivierBlanvillain
Copy link
Owner Author

OlivierBlanvillain commented Mar 29, 2017

I would personally pick referential transparently over the alternatives any day of the week, but what's better ultimately comes down to preferences, so let's not get into that 😉.

Regarding FRP vs data-binding, I think the two are not mutually exclusive! Indeed, putting Vars aside, the Binding.scala APIs are a subset of what's implemented in mhtml. In addition to pure/map/flatMap on Rx[T] and := on Var[T] (which is also <: Rx[T]), we also have a bunch of elm inspired operators like dropRepeats, merge and foldp. Any the two play quite nicely together!

When handling users inputs, say button clicks, you essentially have the choice between directly mutating your business logic state on some existing Var (which would be more imperative), or modeling these clicks as a Rx[Unit] to be composed with other Rxs (that would be more functional). Then all Rxs can be binded to HTML nodes, independently of whether they were constructed imperatively with data-binding style assignments, or compositionally with FRP-style methods.

Anyway, that's a really interesting discussion, I should factor some of this out in the FAQ as it nicely summaries the hybrid nature of the design 😄

@Atry
Copy link

Atry commented Mar 29, 2017

Maybe data-binding can be seen as a special case of FRP.
However, if you need only data-binding, you can perform some optimizations. For example, caching values, preventing re-calculate when the cached value is not changed. https://scalafiddle.io/sf/5vXgQDL/1

And of course, patching data on BindingSeq is another optimization.

carueda added a commit to mbari-org/gpdviz-sjs that referenced this issue Aug 16, 2017
@sometao
Copy link

sometao commented Mar 17, 2018

Any update for this issue?

@bbarker
Copy link
Contributor

bbarker commented Mar 17, 2018 via email

@fdietze
Copy link

fdietze commented Mar 21, 2018

I found some relevant papers which could help building a good alternative to vdom: raquo/Laminar#14

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants