Html.Lazy and extensible records

· Allanderek's blog

#elm #programming

I really like using Elm's extensible records. There is a little debate about how best to utilise them. It seems clear they should be used for narrowing the types of function arguments. Narrowing the type of an argument to a function often makes it significantly more general. Here's a quick example, suppose you have a User type in your application, you might have only a few of them, say a list of friends of the current user or something. So you might write a function to find a particular user in a list:

 1type alias UserId = String
 2type alias User =
 3    { id : UserId
 4    , name : String
 5    , ...
 6    }
 7
 8findUser : UserId -> List User -> Maybe User
 9findUser userId users =
10    List.find (\u -> u.id == userId) users

All great, but often the id part of an entity comes from the database, and we actually have many such entities in our application, so we can actually make the findUser function much more general by using an extensible record type for the entity type:

1findEntity : comparable -> List { a | id : comparable } -> Maybe { a | id : comparable }
2findEntity id entities =
3    List.find (\e -> e.id == id) entities

Now this works for all of the entities in your program. The second use of extensible record types is to actually model data. At one point Evan suggested they should not be used for that, so you tend to get a bit of pushback against this idea, but clearly extensible records are useful for some data modelling issues.

Anyway, what I really wanted to talk about today was a small conflict between the use of extensible records and Html.lazy. The idea behind Html.lazy is a very good one. Often in your Elm application you will render some complicated, or expensive, element, perhaps it is a list of something, say products in an e-commerce store, or footballers in a fantasy football application. Many messages which invoke the update function will not change the list of products/footballers etc. So it is a shame to re-render this list on every view. That's where Html.lazy comes in, if you use Html.lazy instead, then Elm's runtime memoizes the rendered list and only re-renders it, if the inputs to the renderer changes. Let's make this a bit more concrete with some code, let's suppose we're authoring a fantasy football application:

 1type alias Model =
 2    { players : List Player
 3    , filters : Filters
 4    , entry : Entry
 5    , now : Time.Posix
 6    , ...
 7    }
 8
 9renderPlayers : Filters -> List Player -> Html Msg
10renderPlayers filters players =
11    ...
12
13view : Model -> Html Msg
14view model =
15    Html.div
16        []
17        [ Html.header [] [ ... ]
18        , ...
19        , Html.lazy2 renderPlayers model.filters model.players
20        , ...
21        , Html.footer [] [ ... ]
22        ]

An alternative to this style for renderPlayers is just to take the entire model in, but use extensible records to only record the parts that you care about:

 1type alias Model =
 2    { players : List Player
 3    , filters : Filters
 4    , entry : Entry
 5    , now : Time.Posix
 6    , ...
 7    }
 8
 9- renderPlayers : Filters -> List Player -> Html Msg
10- renderPlayers filters players =
11+ renderPlayers : { a | filters : Filters, players : Players } -> Html Msg
12+ renderPlayers { filters, players } =
13    ...
14
15view : Model -> Html Msg
16view model =
17    Html.div
18        []
19        [ Html.header [] [ ... ]
20        , ...
21-        , Html.lazy2 renderPlayers model.filters model.players
22+        , Html.lazy renderPlayers model 
23        , ...
24        , Html.footer [] [ ... ]
25        ]

However, there is a problem here. The Html.lazy is essentially useless, because any update that changes the model at all, will cause the renderPlayers to be re-run, even though neither the filters or the players in the model have been changed.

It's possible that the Elm compiler and run-time could be updated to make this just work, but currently this is a trade-off that you need to take into consideration when deciding how to model your data and construct your views.