# Beyond Html.Lazy's argument limit

**URL:** <https://discourse.elm-lang.org/t/beyond-html-lazys-argument-limit/10211>\
**Category:** Show and Tell\
**Created:** [April 26, 2025, 12:40pm UTC](https://discourse.elm-lang.org/t/beyond-html-lazys-argument-limit/10211 "2025-04-26T12:40:07Z")\
**Posts on this page:** 11\
**Page:** 1

<div class="post-metadata">

**Author:** ![jfmengels](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/jfmengels/32/3124_2.png) [@jfmengels](https://discourse.elm-lang.org/u/jfmengels)\
**Post date:** [April 26, 2025, 12:40pm UTC](https://discourse.elm-lang.org/t/beyond-html-lazys-argument-limit/10211/1 "2025-04-26T12:40:07Z")

</div>

Tired of being limited to 8 arguments with `Html.Lazy`? Then please take a look.

> **[jfmengels' blog](https://jfmengels.net/beyond-elm-lazy-arguments/)**
>
> Written by Jeroen Engels, author of \[elm-review\](https://elm-review.com/). If you like what you read or what I made, you can follow me on \[BlueSky\](https://bsky.app/profile/jfmengels.bsky.social)/\[Mastodon\](https://mastodon.cloud/@jfmengels) or...

---

<div class="post-metadata">

**Author:** ![jfmengels](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/jfmengels/32/3124_2.png) [@jfmengels](https://discourse.elm-lang.org/u/jfmengels)\
**Post date:** [May 1, 2025, 10:30am UTC](https://discourse.elm-lang.org/t/beyond-html-lazys-argument-limit/10211/2 "2025-05-01T10:30:52Z")

</div>

I have turned this into an proposal in `elm/html`.

> <https://github.com/elm/html/issues/257>
>
> The \`Html.Lazy\` module is a great module, and one of the commonly reported probl…ems with it are the arbitrary limit of up to 8 arguments. There are workarounds for this but they have their limits and are not necessarily easy to apply.
> 
> In one of my projects, we have introduced a new function called \`lazyShallow\` which takes a function (that returns \`Html msg\`) and a record as arguments.
> 
> \`\`\`elm
> lazyShallow : (record -\> Html msg) -\> record -\> Html msg
> lazyShallow fn record =
> fn data
> \`\`\`
> 
> We then patch the compiled output to make it so that that the field values 
> 
> \`\`\`js
> var $author$project$Html$LazyExtra$lazyShallow = F2(function(func, record)
> {
> var args = \[func\];
> for (var key in record)
> {
> args.push(record\[key\]);
> }
> return \_VirtualDom\_thunk(
> args,
> function() {
> return func(record);
> }
> );
> });
> \`\`\`
> 
> We have had great success with this approach. We don't feel limited by the number of arguments anymore and we have removed complex workarounds that made our code more brittle than we would have liked (workarounds are described in https://elmcraft.org/faqs/html-lazy-not-working/).
> 
> I believe this would be a great addition to \`elm/browser\`, using this repository's access to kernel code (instead of patching the JS output).
> 
> For more reading, I've written about this approach and the problems it solves \[in this article\](https://jfmengels.net/beyond-elm-lazy-arguments). And for more implementation details, I have made a demonstrable repository (https://github.com/jfmengels/elm-lazy-shallow).
> 
> \### Record field order
> 
> In the implementation we use in the project, \[we sort the keys alphabetically\](https://github.com/jfmengels/elm-lazy-shallow/commit/fa5ca4e159b06559a295cda7cc805132903a1782). From some testing (which could be more extensive), it seems that Elm's record fields order is very consistent, and that the sorting is not necessary.
> 
> \### Function type
> 
> One current limitation of the approach we've taken is the type annotation for the type. I've described the problem in more detail in \[this section\](https://jfmengels.net/beyond-elm-lazy-arguments#lazydummy), but roughly, this function takes an Elm record, but there's no way in Elm's type system to indicate that an argument is a record, without specifying \*\*at least one\*\* specific field it should contain.
> 
> I see several options to this:
> 1. The language adds syntax that supports marking something as a record. It could be \`{ record | ...}\`, \`{ record }\` or something else entirely.
> 2. Add a dummy field. That's the approach we've taken on my project which feels very okay, but feels lacking coming from a core package
> 3. Have the function take any data but if the argument is not an Elm record, then it either silently skips lazification or defaults to the \`Html.Lazy.lazy\` behavior. I think we could probably detect that with code like
> \`\`\`js
> typeof record === "object"
> && record.$ === undefined // it's not a custom type
> \`\`\`
> but I'm sure there are some exceptions to this, which could be tricky to figure out. For instance, if the argument is a \`Json.Decode.Value\` for instance, then maybe the check could be difficult, I'm not entirely sure.

---

<div class="post-metadata">

**Author:** ![gampleman](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/gampleman/32/43_2.png) [@gampleman](https://discourse.elm-lang.org/u/gampleman)\
**Post date:** [May 8, 2025, 2:49pm UTC](https://discourse.elm-lang.org/t/beyond-html-lazys-argument-limit/10211/3 "2025-05-08T14:49:37Z")

</div>

One thing I don’t quite understand is why is lazy tied to elm/html? Shouldn’t there be a general memoization primitive?

---

<div class="post-metadata">

**Author:** ![lydell](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/lydell/32/178_2.png) [@lydell](https://discourse.elm-lang.org/u/lydell)\
**Post date:** [May 8, 2025, 3:41pm UTC](https://discourse.elm-lang.org/t/beyond-html-lazys-argument-limit/10211/4 "2025-05-08T15:41:31Z")

</div>

Wow! That’s an exciting thought.

Maybe like this:

```elm
memoize ({ a | lazyDummy : () } -> b) -> { a | lazyDummy : () } -> b
memoize f record =
    f record

```

The implementation of that function would be replaced with something like:

```js
var _cache = new Map();
var memoize = F2(function(f, record)
  {
    var lastKey;
    var fCache = _cache.get(f);
    var cache = fCache;
    var hasCache = true;
    if (cache === undefined)
    {
      hasCache = false;
    }
    else
    {
      for (var key in record)
      {
        lastKey = key;
        cache = cache.get(record[key]);
        if (cache === undefined)
        {
          hasCache = false;
          break;
        }
      }
    }
    if (hasCache)
    {
      return cache;
    }
    
    var result = f(record);

    var parent = fCache;
    if (fCache === undefined)
    {
        parent = new Map();
        _cache.set(f, parent);
    }
    for (key in record)
    {
      var value = record[key];
      var cache = parent.get(value);
      if (cache === undefined)
      {
        cache = key === lastKey ? result : new Map();
        parent.set(value, cache);
      }
      parent = cache;
    }

    return result;
  });

```

Note: I wrote this code right now and I haven’t tested it.

Also note that the code uses `Map`, which none of Elm’s current JS does (but should be fine IMO).

---

<div class="post-metadata">

**Author:** ![lydell](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/lydell/32/178_2.png) [@lydell](https://discourse.elm-lang.org/u/lydell)\
**Post date:** [May 8, 2025, 3:54pm UTC](https://discourse.elm-lang.org/t/beyond-html-lazys-argument-limit/10211/5 "2025-05-08T15:54:43Z")

</div>

This might be a better implementation:

```js
var _cache = new Map();
var memoize = F2(function(f, record)
  {
    var prev = f;
    var parent = _cache;
    for (var key in record)
    {
      var value = record[key];
      var cache = parent.get(prev);
      if (cache === undefined)
      {
        cache = new Map();
        parent.set(prev, cache);
      }
      prev = value;
      parent = cache;
    }
    
    var ret = parent.get(prev);

    if (ret !== undefined)
    {
      return ret;
    }
    
    ret = f(record);
    parent.set(prev, ret);
    return ret;
  });

```

Edit: Will this leak memory over time? I only add to the map, never remove from it.

---

<div class="post-metadata">

**Author:** ![gampleman](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/gampleman/32/43_2.png) [@gampleman](https://discourse.elm-lang.org/u/gampleman)\
**Post date:** [May 8, 2025, 4:36pm UTC](https://discourse.elm-lang.org/t/beyond-html-lazys-argument-limit/10211/6 "2025-05-08T16:36:05Z")

</div>

You’d probably want a library with different options. Html.Lazy is a LRU-1 cache strategy afaict, but in general memoization allows you to trade memory for CPU, and exactly which caching strategy you choose will dictate how that trade will go.

But even having a LRU-1 general purpose memoization would be pretty helpful for performance work in Elm.

---

<div class="post-metadata">

**Author:** ![jfmengels](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/jfmengels/32/3124_2.png) [@jfmengels](https://discourse.elm-lang.org/u/jfmengels)\
**Post date:** [May 8, 2025, 7:40pm UTC](https://discourse.elm-lang.org/t/beyond-html-lazys-argument-limit/10211/7 "2025-05-08T19:40:44Z")

</div>

There was a general memoization primitive in Elm 0.18 with [GitHub - elm-lang/lazy: Lazy Evaluation in Elm](https://github.com/elm-lang/lazy), but it’s not available in Elm 0.19 mostly because of memory leak concerns as far as I understand it. You can probably use some inspiration from here if you’d like to explore this (especially if you’d like to stick to ES5, `Map` and `WeakMap` came later).

I’m sure there are cases where this could be looked into again, but this is not the way that lazy/caching works in Elm. I’m in the process of writing an explainer article about it (hopefully getting it out today or at least before the end of the weekend).

But in a rough summary, the result of the previous lazy computation is stored in the virtual DOM itself, and is re-covered (on cache hits) when diffing the old and new virtual DOMs. This has the nice property that it’s fast and there is no memory leak.

Because of this, Html.Lazy is indeed tied to `elm/html` / `elm/virtual-dom`.

---

<div class="post-metadata">

**Author:** ![gampleman](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/gampleman/32/43_2.png) [@gampleman](https://discourse.elm-lang.org/u/gampleman)\
**Post date:** [May 9, 2025, 9:04am UTC](https://discourse.elm-lang.org/t/beyond-html-lazys-argument-limit/10211/8 "2025-05-09T09:04:47Z")

</div>

In my mind there seems to me a confusion between some concepts going on there.

1. [Lazy evaluation](https://en.wikipedia.org/wiki/Lazy_evaluation) is the practice of deferring computing a value until it is actually needed (usually for an effect). As the README there says, this is perfectly possible by just using thunks.
2. [Memoization](https://en.wikipedia.org/wiki/Memoization) is caching the results of a pure function to avoid recomputing it.
3. [Sharing (or persistent data structures)](https://en.wikipedia.org/wiki/Persistent_data_structure) is when multiple ways to access a value all share a reference to the same underlying memory.

These are all completely orthogonal. The elm-lang/lazy library seems to mix these up in a particular way that both seems not super useful and introduces various problems of its own. In comparison the `Html.Lazy` library really does just memoization, but with the special sauce of having a caching strategy where cache hits depend on the position in the DOM tree (and the not insignificant limitation that your memoized function needs to return the `Html` type).

So what I’m wondering is simply if there is space for a library with pretty much the same design as Html.Lazy, but where a) the return type is `a` and b) the results are stored on the function object itself (which also means that we don’t need to worry about `==` the functions).

---

<div class="post-metadata">

**Author:** ![lydell](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/lydell/32/178_2.png) [@lydell](https://discourse.elm-lang.org/u/lydell)\
**Post date:** [May 9, 2025, 7:28pm UTC](https://discourse.elm-lang.org/t/beyond-html-lazys-argument-limit/10211/9 "2025-05-09T19:28:09Z")

</div>

It would be useful for Advent of Code at least!

---

<div class="post-metadata">

**Author:** ![allanderek](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/allanderek/32/1360_2.png) [@allanderek](https://discourse.elm-lang.org/u/allanderek)\
**Post date:** [May 19, 2025, 9:13am UTC](https://discourse.elm-lang.org/t/beyond-html-lazys-argument-limit/10211/10 "2025-05-19T09:13:30Z")

</div>

I think this is a great summary of the three distinct meanings of ‘lazy’.  
I’m not sure they are _entirely_ orthogonal, in particular number 1. is most useful if you have binary decision as to whether or not to compute the value. But, for the most part, lazy evaluation is helpful in the cases that whether or not a value gets computed is ‘complex’. This likely means that if you ‘just use thunks’ you’ll end up with situations in which you compute the thunk more than once.

Lazy evaluation at the language level means that you compute a value at most once. If you implement this yourself in an eager language by simply using a thunk, the danger is that you will sometimes compute the value more than once. But if you add “2. Memoization” together with “1. Lazy evaluation” then you _kind of_ get the best of both worlds, in that the expensive computation is computed _at most_ once.

I realise @gampleman knows all this, and his suggestion regarding a library where the computed result is stored on the function object, is essentially adding lazy computation to an eager language (and would have to be done as a privileged library). I think there are some papers regarding this, for example adding lazy evaluation to O’caml, and there is an interesting paper on [Wadler: Language design](https://homepages.inf.ed.ac.uk/wadler/topics/language-design.html) " How to add laziness to a strict language, without even being odd"

---

<div class="post-metadata">

**Author:** ![system](https://canada1.discourse-cdn.com/flex035/uploads/elm_lang/original/1X/50a05e53677a2c3b47776d7abd0f113eb50193a1.png) [@system](https://discourse.elm-lang.org/u/system)\
**Post date:** [May 29, 2025, 9:13am UTC](https://discourse.elm-lang.org/t/beyond-html-lazys-argument-limit/10211/11 "2025-05-29T09:13:36Z")

</div>

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