Closed bbarker closed 2 years ago
Note I'm still trying for the "Component of List of Components" approach, which implies an Rx embedding a list of Rxs - it seems to be a fairly tricky problem, but I'm hoping monadic-html may be able to overcome this problem and make modularity of components relatively easy compared to other FRP frameworks.
Rather than implementing a function of a different name, should we just implement our own optimized flatten, instead of relying on Cats's flatten?
Well, I went with merge
in #72 to keep a consistent style and avoid possible conflicts with cats.
I added Semigroup.combineAllOption
which seems to simplify things, however performance is still an issue: I think there are a few redundant executions I'm having trouble spotting, despite liberally using dropRepeats
.
For instance, upon loading the page, we can see most printlns
are printed three times. As an example, we can see (TodoList(All,#/,DropRep(Var(List()))),List(),None)
shows up three times upon initial page load ( source line ). This is a bit mysterious to me since the map
call is immediately preceded by dropRepeats
, so I wonder if it is an issue of quality on TLCMonitors
. However, I doubt this is the case, since we see other things being executed three times as well.
I think the next step would be to turn the example into a test and minimize it to understand what is the source of the redundant executions...
Thanks for the suggestion, much better than just clicking around. I got it from 3 executions to 2 executions here.
The reasons for this seem to be that allTodos
are indirectly being used elsewhere, so there is no need to include its execution separately under val todoapp: Node = ...
. Probably wouldn't have written it that way now but it was a bit of code smell from earlier days of my attempt in the todomvc project.
I also bring this up at the time because I seem to like the general style of using imitate
in this style where we only have two linked Rx's, instead of 3.
Finally, as a side note, I increased the stack size for the compiler in .sbtopts
due to scala.js crashing: https://github.com/scalatest/scalatest/issues/1164
I'll keep poking around trying to find the last redundant execution.
I found the other culprit, though I'm not sure how to best deal with it just yet:
val mainSection: Component[List[UpdateEvent]] = {
val todoUpdates = Var[List[UpdateEvent]](Nil)
val display = allTodos.map(todos => if (todos.isEmpty) "none" else "")
val mainDiv =
<section class="main" style:display={display}>
<ul class="todo-list">
{ todoListComponents.view }
</ul>
</section>
Component(mainDiv, todoUpdates.dropRepeats)
}
In this snippet, both the inclusion of {display}
and the inclusion of { todoListComponents.view }
cause separate firings of "the main Rx cycle"; todoListComponents
and allTodos
are intertwined - I'm most interested if there is a general solution to this problem that isn't specific to the todomvc example.
@OlivierBlanvillain I recall you mentioning that you had a major insight about imitate
on gitter as it relates to this PR - just wondering if you could expand on that here - thanks!
Hey, sorry for the long silence... My insight on imitate
turned out to be incorrect, I couldn't find anything wrong with the current behavior of imitate
.
My current assumption is that all the duplication you see is due to streams being "interpreted" more than once when they are attached to several mount point in the DOM. I see two possible solutions to this problem.
Give control to the user over what to share and what to not share between several interpretation of a same stream. Something like a .shared
which "fires" the underlying stream and returns a new Rx
with a guarantee that all computations before that point are shared between all interpretations. I don't really like this solution as it forces users to think in term of mutations and events in a system that is otherwise pure and RT...
Implement a optimizing interpreter that automatically inserts .shared
when it doesn't break semantics (without changing anything to the public API). The idea start from the observation that certain streams (or chunks of streams), can be systematically cached and shared under certain purity assumptions. For instance, given val out = source.map(f1).map(f2)
, and assuming purity of f1
and f2
, we know that the following two programs are equivalent (and it should be possible to prove this equivalence):
val out = source.map(f1).map(f2)
out.impure.foreach(println)
out.impure.foreach(println)
val out = source.map(f1).map(f2)
val tmp = Var(null)
out.impure.foreach(tmp.:=)
tmp.impure.foreach(println)
tmp.impure.foreach(println)
// Additional care must be taken to generate proper Cancelables...
But this is obviously not true in the general case, for instance in the presence of foldp
. The assumption with this second solution is that given a sufficiently smart optimizer we could get all the benefits of the first solution without requiring any user knowledge.
Developing a bit on this idea, it's possible to categorize the current API in three categories, always sharable, tail sharable and never sharable:
map
, zip
, sampleOn
, imitate
, dropRepeats
keepIf
, merge
foldp
, flatMap
(Note: I think flatMap
can be made more precise by saying that it "inherits" the shareability of whatever is returned by it's function. For instance, flatMapping on something that always returns a Rx made of map
and zip
results in something always sharable.)
Tail sharable means that a stream can be shared after it emitted it's second element. At first glance it might not be obvious why keepIf
is only tail sharable, here is a small diagram to illustrate (where "•" represents the registration point):
val numbers: Rx[Int]
val even: Rx[Int] = numbers.dropIf(_ % 2 == 0)(-1)
// numbers => 0 0 3 4 5 6 ...
// even1 => •-1 3 5 ...
// even2 => •-1 5 ...
even1
emits -1, 3, 5
while even2
emits -1, 5
, so not everything can be shared between the two streams. In other words, we cannot simply implement even2
by "hooking" into even1
upon registration, but we could do that after the 5
is emitted! A similar argument we can be used to show that merge
is not always sharable but only tail sharable
.
It's then interesting to think how this sharability properties compose. I have the intuition than if a Rx only compose of always sharable parts is itself always sharable, while containing any never sharable component makes the entire Rx non sharable. It gets funny when mixing always sharable and tail sharable components, it seams that the result is not just tail sharable, but something stronger where each of the intermediate streams have emitted at least one element. As an example, consider val out = rx1.dropIf(_ % 2 == 0)(-1).merge(rx2.dropIf(_ % 2 == 0)(-1))
. It is possible to have out
emit several values without reaching sharability, or to have each "leaf" Var to emit several values without reaching sharability (the diagrams get a bit messy to draw, but I have have them on a white board :P).
Supersedes #53
Work has been done to create a ComponentList based on Map instead of List, which should improve performance somewhat, as well as improving organization of code.
There are a few major issues at this time that I'm unsure of how to debug:
dropRepeats
- not sure of the best way to debug this, though I wonder if it may be a referential vs structural equality issue. To see this, filter out console logs of 'yield' in a browser (for example).Some(UpdateEvent(Todo(asdf,false),Todo(asdf,true)))
will be shown in the console, however, it doesn't actually become checked off in the model; this can be seen by the fact that the firstTodo
argument ofUpdateEvent
never changes no matter how many times it is clicked - it is always false. Maybe this is a simple logic error and I can't see the tree for the forrest, but I suspect something more difficult along the lines of Rx propagation not happening cyclically where I assume it should be, or similarly, Rx's may not be subscribed appropriately to one another somewhere along the way.