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

Html.Lazy always recomputes inner functions #201

Open
showell opened this issue Nov 17, 2019 · 5 comments
Open

Html.Lazy always recomputes inner functions #201

showell opened this issue Nov 17, 2019 · 5 comments

Comments

@showell
Copy link

showell commented Nov 17, 2019

The virtual DOM will always recompute any function that's inside a let, even if it's wrapped inside Html.Lazy.lazy.

Here is the SSCCE:

https://github.com/showell/elm-start/commits/html-lazy-is-broken-for-let-functions

There are two commits to note here:

  • The last commit shows how it should work. Run this without building index.html. Instead, just use the index.html that is checked in. (If you look at the commit, you can see my proposed changes to index.html to avoid the bug, and there's some more detail.)

  • The second to last commit is the repro. Or, really, you can actually just rebuild the last commit. Either way, if you don't use my modified index.html, you can type around in the textarea and see it recomputing lotsOfFreds over and over again.

The example in the repro is a full program, but you can get the gist here:

 66 view : Model -> Browser.Document Msg
 67 view model =
 68     let
 69         lotsOfFreds : String -> Html Msg
 70         lotsOfFreds s =
 71             let
 72                 -- We should only see this once, but it happens every time
 73                 -- you type in the textarea
 74                 _ =
 75                     Debug.log "actually calling lotsOfFreds" ""
 76             in
 77             Html.pre [] [ Html.text (String.join "\n" (List.repeat 100000 s)) ]
 78
 79         body =
 80             [ Html.textarea
 81                 [ onInput DontActuallyUpdateTheModel ]
 82                 [ Html.text "type in here to repro bug (and open debugger)" ]
 83             , Html.Lazy.lazy lotsOfFreds model.fred
 84             ]
 85     in
 86     { title = model.title
 87     , body = body
 88     }

The problem happens deep in the virtual DOM implementation. The virtual DOM is comparing two different copies of lotsOfFreds.

3045     // Now we know that both nodes are the same $.
3046     switch (yType)
3047     {
3048         case 5:
3049             var xRefs = x.l;
3050             var yRefs = y.l;
3051             var i = xRefs.length;
3052             var same = i === yRefs.length;
3053             while (same && i--)
3054             {
3055                 same = xRefs[i] === yRefs[i];
3056             }
3057             if (same)
3058             {
3059                 y.k = x.k;
3060                 return;
3061             }
3062             y.k = y.m();
3063             var subPatches = [];
3064             _VirtualDom_diffHelp(x.k, y.k, subPatches, 0);
3065             subPatches.length > 0 && _VirtualDom_pushPatch(patches, 1, index, subPatches);
3066             return;
@showell
Copy link
Author

showell commented Nov 17, 2019

I think this is technically a compiler bug, by the way. The code in Html.Lazy (or, more precisely,_VirtualDom_diffHelp) is doing the right thing to make sure that the two functions in the thunk are, in fact, the same. The only reason the two functions aren't the same here is due to how the JS is emitted (and due to the fact that JS creates new copies of the function each time for some reason).

I doubt this is browser-specific behavior, but I tested this only on FireFox.

@showell
Copy link
Author

showell commented Nov 17, 2019

Also, just to make things even more complex here, if the inner view helper did close on any Elm variables besides the arguments (it doesn't here, but it could in other scenarios), then the Elm compiler would have to emit the inner function the way it does now. In that case there's no way that I can think of to emit code that is friendly to Html.Lazy.lazy. In other words Html.Lazy would always have to punt on optimizations due to seeing "different" functions (even though it's the same JS code). I think in most of these scenarios the relevant model data would be changing anyway, but sometimes it will be valid code that just uses maybe one mostly non-changing field from the model. So it's possible that the solution here is to just prevent folks from applying Html.Lazy.lazy to any function in the let.

@showell showell changed the title Html.Lazy is broken for inner functions Html.Lazy always recomputes inner functions Nov 17, 2019
@showell
Copy link
Author

showell commented Nov 17, 2019

I opened elm/compiler#2020 in hopes that the compiler could be changed here to support Html.Lazy better. I am somewhat resigned to the fact, after talking on Slack, that folks just consider this a known limitation. If that's the case, then maybe there's a path to make the documentation even more clear here. Another person suggested trying to solve this first with something like elm-analyse.

@z5h
Copy link

z5h commented Mar 17, 2021

As you can see, equality here is by reference, not by value. Anything in the let (a function, a list, a record) will not be equal by reference to its value the last incarnation of the function.
The solution is to move functions and any non-primitive value to top-level function/value, or store it in the model and make sure it passes unscathed to lazy.

@Birowsky
Copy link

I'm willing to commission somebody to implement a version of lazy that works with records. This would be an invaluable addition for me. It would ignore the record and compare references of all the members of the record.

Many of the views in my apps take big responsibilities, and consequently, take big records as configurations. It makes things waaay more readable if the arguments are "named" this way.

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

3 participants