How does Elm.Lazy work?

#1

I was wondering how Elm.Lazy worked. For example in this:

import Elm.Lazy exposing (lazy)

view model =
  [ lazy viewSomething 1
  , lazy viewSomething 2
  ]

Would lazy “cache” the first call with parameter value “1”, then it has to evaluate the next call to “viewSomething”, check it has started parameter “1”, see the new parameter is different, and therefore call viewSomething, and store that it has called it?

I.e. the lazy in this example is utterly useless?

#2

Every call to lazy is independant. Technically, it returns a structure that contains the view function and all the arguments.

On the first render, the view function is called with the arguments. The result is saved for the next render, where the view function and arguments are compared. If any have changed, the view function is called again. Otherwise, the virtual DOM uses the previous return value.

All this logic is part of elm/virtual-dom: https://github.com/elm/virtual-dom/

So to answer your question, those two lazy calls don’t interact with each other!

#3

Yes, but if I call the same function in the same view twice, which I did in my example, how will it behave?

Looks like that won’t work wouldn’t it?

#4

The cache is based on the DOM structure, so every node gets its own cache. Those two are siblings, so the virtual DOM knows they are different. (Things get more interesting if the structure CHANGES. But that’s what virtual DOMs are designed to figure out).

So your example should definitely work correctly! You can throw a Debug.log in the view function to see when it’s actually getting called, vs the top-level view function.

#5

There is long explanation about DOM, VirtualDOM and Html.lazy here in Elm guide: https://guide.elm-lang.org/optimization/lazy.html

#6

Well, yes the result is cached, but lazy only caches on the basis of its parameters right? So the function name is a lookup in a Dict (I’m guessing), and it compares the parameters.

So I can’t see my example providing any benefit (it will work, but it will do no useful caching).

#7

No, it also uses the location of the DOM node. So in your example, the lazy calls are independent because they are different DOM nodes. lazy doesn’t use a global cache like a plain memoized function.

I would highly recommend that link @malaire posted, it goes over how a virtual DOM works before explaining lazy, in case you aren’t familiar with the inner workings.

#8

This is easy to test using Debug.log :slight_smile:
https://ellie-app.com/5vcMQQkM7xSa1
If it wasn’t working, then it would print to the console on every update.

2 Likes
#9

To expand on it a little bit, it works with integers here because they are referentially equal in JS.

div [] [
   lazy viewSomething 1
  ,lazy viewSomething 2
]

That is, 1 === 1 and 2 === 2 in JS, you can also do it with strings and booleans if you are passing them around because "somestring" === "somestring". What you can’t do is use it with objects or arrays because they are not the same reference:

{ a : 1 } === { a : 1}
// False

So if you were to change the code to this, it would break

div [] [
   lazy viewSomething { val = 1 }
  ,lazy viewSomething { val = 2 }
]

You could maintain the reference equality if you declare them at the top level

something = 
    { val = 1 }

anotherSomething = 
    { val = 2 }

view = 
    div [] [
       lazy viewSomething something
      ,lazy viewSomething anotherSomething
    ]

A common gotcha with lazy rendering is that it is very easy to break in your update function, you have to make sure that you keep reference equality unless the value really changed, like say you have

type alias Model = { count: Int }

And you were trying to use lazy viewSomething model. In you update function you’d have to be careful to not change the Model reference unless the value is really changing.

UpdateCount count ->
    { model | count = count }

The code above always returns a new model, so lazy will never work even if the new count and the old count are the same, instead you have to do

UpdateCount count ->
    if count == model.count then
        model
    else 
        { model | count = count }

In this example lazy wouldn’t do much, but if you are creating a ton of nodes it can make a big difference.

If you are using the lazy2, lazy3, lazyN variants it can be hard to know what parameter is preventing lazy rendering from happening, in 0.18 I used to use a patched version of virtual-dom to print it out, the important bits of code are here https://github.com/elm/virtual-dom/blob/master/src/Elm/Kernel/VirtualDom.js#L738 where it checks for reference equality.

2 Likes
#10

Ah, I hadn’t seen that everywhere. And obviously @Herteby’s example is conclusive.

Very interesting, I think this improved my understanding a lot.

closed #11

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.