Discussion:
[elm-discuss] Passing whole state to each view function
Tomaž Žlender
2017-06-21 17:03:24 UTC
Permalink
Is it performance expensive if each view function gets a whole state as an
argument? So instead of passing only a portion of a data that view function
needs for rendering, each function gets whole state and inside that
function another function is called first that transforms data in a format
needed to generate view.
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Aaron VonderHaar
2017-06-22 04:47:55 UTC
Permalink
There is no performance penalty to passing the entire model to all nested
view functions. Elm will simply pass the reference to the object around,
so the performance of passing the entire model vs passing just the
necessary parts should be equivalent.

However, the downside of doing passing the entire model everywhere is that
you have to keep the entire model in your head when looking at any of your
view functions. In contrast, if you limit what data you pass to each
function, then it limits the possibility of bugs and makes it easier to
focus when you work on the smaller functions. The smaller functions will
also be potentially more reusable and it will be easier to set up initial
data when testing. This also makes it easier to extract modules later,
since if all the functions take a Model, then any module you extract would
also need to import Model and any other modules that data in the model
depends on.

I also personally find that having the caller process the data (instead of
having the called function transform the entire model into the data that it
needs to render) usually leads to a nicer separation of responsibilities.
Post by Tomaž Žlender
Is it performance expensive if each view function gets a whole state as an
argument? So instead of passing only a portion of a data that view function
needs for rendering, each function gets whole state and inside that
function another function is called first that transforms data in a format
needed to generate view.
--
You received this message because you are subscribed to the Google Groups
"Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an
For more options, visit https://groups.google.com/d/optout.
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Mark Hamburg
2017-06-22 18:16:26 UTC
Permalink
Also note that while the approach of transforming your model into a more
view-specific data structure — e.g., extracting the pieces of interest —
can be a great pattern in terms of code clarity, it works against using
Html.Lazy. For background on the pattern, see Charlie Koster's piece on
selectors and view models:
https://medium.com/@ckoster22/upgrade-your-elm-views-with-selectors-1d8c8308b336
.

Concretely, structuring your code as:

toViewData : Model -> ViewData

displayViewData : ViewData -> Html Msg


and thence

view : Model -> Html Msg
view =
toViewData >> displayViewData


works well — particularly if it allows for putting the view function into
its own module where you might feel more free about doing lots of exposes
for Html, Css, etc. However, it also means that the view function and any
pieces thereof cannot effectively be lazy based on the ViewData it is
passed because the ViewData instance will be new each time (even if it is
actually equal to the previous value) and Html.Lazy does a naive (also
known as fast) equality check based on instance identity.

If you know how Html.Lazy works, this issue is perhaps obvious in the code
above, but as view functions get bigger to deal with more complicated views
or moves to its own module, I've seen cases where developers think "this is
an obvious place to put in some laziness" only to have it simply result in
more work for the view system.

Why do we even care about laziness? Because it's the functional programming
answer when engineers from other backgrounds say "You re-render the whole
model on every change? Isn't that vastly less efficient than just updating
the parts of the view that changed?" It's also part of how Elm gets the
performance numbers that it gets on UI benchmarks.

So, great pattern. Use with caution.

Further notes below on laziness. Read if interested.

Mark

P.S. The way to get laziness in the above is:

view : Model -> Html Msg
view model =
Html.Lazy.lazy doView model

doView : Model -> Html Msg
doView =
toViewData >> displayViewData

We need to specifically define doView because every time we execute >> we
get a new function instance which will then fail the laziness match.

The unfortunate thing here is that if the model contains pieces that don't
directly affect the display — e.g., a queue of messages to send — then the
laziness also depends on those pieces of the model. Part of the appeal of
the ViewData intermediary is that it projects that stuff away.

P.P.S. Over-engineered solution (though it would also help subscriptions as
well): The implementation strategy that makes Html.Lazy work could probably
be extended to a general recompute pattern with laziness. The basic idea is
that while evaluating the tree, you can look at the previous tree and see
whether a lazy node has the same inputs as before and if so just use the
answer from before. You need a small imperative piece that deals with the
update from one tree to the next but that can live in the runtime.
Something like this could immediately address the potential issue that
calculating subscriptions after every update could be rather expensive
depending on the size of the model and the number of subscriptions. For the
discussion above, one would formalize the notion of view data — with
identity being the simple implementation for converting the model to view
data — and then do a "smart" re-computation of the view data as the first
step in rendering the view.

One could probably even do this without changing the runtime model with
just the addition of a native module akin to the Lazy module. Let's call
that module Cache and give it an interface like the following:

eval : Cache a b -> (a -> b) -> a -> b
-- (eval cache fn a) evaluates to an equivalent value as (fn a); it may
use and/or update the supplied cache

eval2 : Cache2 a b c -> (a -> b -> c) -> a -> b -> c
-- (eval cache fn a b) evaluates to an equivalent value to (fn a b); it
may use and/or update the supplied cache


etc

Caches are mutable but opaque state and their specification should mean
that the only visible effect from them should be reduced code execution.

Enhancements around deeper — but non-exception-throwing -- equality checks
could also help drive things back to states that will match with the
laziness match detectors.

If anyone writes this, let me know and I would be happy to start using it.
:-) If I get there first, I'll say so though obviously this would not be
available via the Elm package manager because of the need to access Elm's
internal data structures.
Post by Aaron VonderHaar
There is no performance penalty to passing the entire model to all nested
view functions. Elm will simply pass the reference to the object around,
so the performance of passing the entire model vs passing just the
necessary parts should be equivalent.
However, the downside of doing passing the entire model everywhere is that
you have to keep the entire model in your head when looking at any of your
view functions. In contrast, if you limit what data you pass to each
function, then it limits the possibility of bugs and makes it easier to
focus when you work on the smaller functions. The smaller functions will
also be potentially more reusable and it will be easier to set up initial
data when testing. This also makes it easier to extract modules later,
since if all the functions take a Model, then any module you extract would
also need to import Model and any other modules that data in the model
depends on.
I also personally find that having the caller process the data (instead of
having the called function transform the entire model into the data that it
needs to render) usually leads to a nicer separation of responsibilities.
Post by Tomaž Žlender
Is it performance expensive if each view function gets a whole state as
an argument? So instead of passing only a portion of a data that view
function needs for rendering, each function gets whole state and inside
that function another function is called first that transforms data in a
format needed to generate view.
--
You received this message because you are subscribed to the Google Groups
"Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an
For more options, visit https://groups.google.com/d/optout.
--
You received this message because you are subscribed to the Google Groups
"Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an
For more options, visit https://groups.google.com/d/optout.
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Max Goldstein
2017-06-22 20:46:48 UTC
Permalink
To state Mark's point clearly: if your view depends on only a small number
of the model's fields, then you are likely to rerender the view only when
some other fields changed, which is unnecessary.

That said, you might not be at the point of needing laziness for
performance. In particular, allocating and copying a new record has a cost
of its own, and you'll have to measure to find if it's worth the lazy call
to the function. Important: because of the virtual DOM, Html.lazy only
saves you the overhead of calling the view function, both its logic and
creating the virtual DOM. If the view function returns virtual DOM that
matches the existing, real DOM, no DOM manipulation is performed, and
that's the real performance win.

So, it is definitely simpler and easier, and possibly no less performant,
to pass the model directly to the view. It would be nice if we could use
the type system to limit which fields of the model the view can see, so we
can isolate it better for understanding and testing. Fortunately there is a
neat trick that does just that:

view : { r | foo : Int } -> Html Msg
view {foo} = ...

This function accepts any record which has a foo field of type Int. If your
model has that field, it can be passed in, but no other fields in the model
are available to the view function.
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Mark Hamburg
2017-06-23 00:26:54 UTC
Permalink
Actually, Html.Lazy also saves the differ the task of comparing the
subtrees when laziness saves us from work, so it can be a big win if UI
updates are a significant chunk of the work.

That said, Max is correct that memory allocation for new structures is also
a cost and is something to be avoided. (Functional programming loves to
allocate memory...)

So, the basic rules of thumb are:


- If you have a big model pieces of which get rendered in various
combinations by other code, try splitting those pieces out and using the
record extension syntax to access just the fields needed while avoiding the
cost of building a separate record.
- If you only need a little bit of information from the model for a
subview, you can just extract it and call the subview function and avoid
the record syntax overhead.
- If your model is structured in a way that makes it easy to keep valid
but harder to draw, that's probably a good trade off — make impossible
states impossible and all that — but may call for using an intermediate
structure between the model and the actual view code. (See Charlie Koster's
piece on "selectors".)
- Laziness can be a big win for both render and diff times with complex
views BUT you need to be aware of how the lazy logic works and not hand it
data that isn't likely to use the same data instances from render to render.
- Laziness can be good as one approaches the leaves and the data
becomes more primitive.
- Laziness can be good near the top if there are large chunks of your
model that won't be changing from cycle to cycle.
- Laziness gets a bit tricky in between these two because it can be
useful to build intermediate structures while rendering and those break
laziness.
- Experiment with the signature to your view functions. The basic idea
of a view function is that it takes some data and returns some HTML. For
example:
- Config records can handle views that need more set up (but you will
likely want to move them out to module scope for performance if possible.
- If you have theme support in your UX, it may be useful to pass the
theme down to all view functions that care as a separate parameter.
- Sometimes it is useful to have view functions return lists of HTML
nodes rather than a single node.

Mark
Post by Max Goldstein
To state Mark's point clearly: if your view depends on only a small number
of the model's fields, then you are likely to rerender the view only when
some other fields changed, which is unnecessary.
That said, you might not be at the point of needing laziness for
performance. In particular, allocating and copying a new record has a cost
of its own, and you'll have to measure to find if it's worth the lazy call
to the function. Important: because of the virtual DOM, Html.lazy only
saves you the overhead of calling the view function, both its logic and
creating the virtual DOM. If the view function returns virtual DOM that
matches the existing, real DOM, no DOM manipulation is performed, and
that's the real performance win.
So, it is definitely simpler and easier, and possibly no less performant,
to pass the model directly to the view. It would be nice if we could use
the type system to limit which fields of the model the view can see, so we
can isolate it better for understanding and testing. Fortunately there is a
view : { r | foo : Int } -> Html Msg
view {foo} = ...
This function accepts any record which has a foo field of type Int. If
your model has that field, it can be passed in, but no other fields in the
model are available to the view function.
--
You received this message because you are subscribed to the Google Groups
"Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an
For more options, visit https://groups.google.com/d/optout.
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Max Goldstein
2017-06-25 03:52:56 UTC
Permalink
Great points, Mark!

I came across this r/elm thread
<https://www.reddit.com/r/elm/comments/6imyje/verifying_laziness/> and they
conclude that Html.lazy only skips a computation if the argument is
*referentially* equivalent to some previous value. If true, this would
invalidate the *toView* approach in Mark's first post on this thread.

I think this suggests having viewstate already split out in the model, i.e.
a record or records. Then you pass each smaller record to a view function.
But the idea of defining your model in a way to make your view functions
more performant.... is not a pleasant one.
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Mark Hamburg
2017-06-25 18:36:34 UTC
Permalink
Thanks for the term "referentially equivalent". It's a nice counterpart to
"referentially transparent". (Long-time LISPers would know these as "EQ" v
"EQUAL".)

Mark
Post by Max Goldstein
Great points, Mark!
I came across this r/elm thread
<https://www.reddit.com/r/elm/comments/6imyje/verifying_laziness/> and
they conclude that Html.lazy only skips a computation if the argument is
*referentially* equivalent to some previous value. If true, this would
invalidate the *toView* approach in Mark's first post on this thread.
I think this suggests having viewstate already split out in the model,
i.e. a record or records. Then you pass each smaller record to a view
function. But the idea of defining your model in a way to make your view
functions more performant.... is not a pleasant one.
--
You received this message because you are subscribed to the Google Groups
"Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an
For more options, visit https://groups.google.com/d/optout.
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
ajs
2017-07-12 09:28:24 UTC
Permalink
However, the downside of doing passing the entire model everywhere is that you have to keep the entire model in your head when looking at any of your view functions.
This is correct, but how can you avoid passing the whole model to all views if the data model isn't structured similarly to your view hierarchy? The idea of doing pre-processing on your data into a view-friendly structure seems to add overhead and complexity, while also more or less duplicating your view structure in two places: the view stack, and the view data.

We have found that as apps grow in size, it is unreasonable to couple a data model's internal structure to the requirements of a UI, and thus passing The whole model is the only valid option for most view functions. The exception are leaf nodes down the line that have few or no children, in which case they can receive focused inputs only.
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Mark Hamburg
2017-07-21 17:04:17 UTC
Permalink
Per the other thread discussing this this morning, I don't know that
passing the whole data model to all of the view-side functions is that
horrendous. (We pass around a UXConfig structure in all of our views to
provide access to translation functions amongst other things and the
biggest pain is that I'm not certain that making it he first parameter by
convention was the right choice.) Passing the whole data model feels icky,
but you can document the dependencies using different modules to access
different parts of the data model and you can still keep data model
mutation out of the views by emulating the command mechanism.

That said, there are, definitely friction points where being able to
subscribe to pieces of the data model would be a significant improvement.
See my other message for some examples of these. But the friction is not in
needing to pass the whole data model through the rendering process.

Mark
Post by Aaron VonderHaar
However, the downside of doing passing the entire model everywhere is
that you have to keep the entire model in your head when looking at any of
your view functions.
This is correct, but how can you avoid passing the whole model to all
views if the data model isn't structured similarly to your view hierarchy?
The idea of doing pre-processing on your data into a view-friendly
structure seems to add overhead and complexity, while also more or less
duplicating your view structure in two places: the view stack, and the view
data.
We have found that as apps grow in size, it is unreasonable to couple a
data model's internal structure to the requirements of a UI, and thus
passing The whole model is the only valid option for most view functions.
The exception are leaf nodes down the line that have few or no children, in
which case they can receive focused inputs only.
--
You received this message because you are subscribed to the Google Groups
"Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an
For more options, visit https://groups.google.com/d/optout.
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Raoul Duke
2017-07-21 19:08:32 UTC
Permalink
if passing the whole db, i wonder if i want something like structural types
on the consuming apis to keep things more clear & safe & sane.
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Simon
2017-08-27 07:08:58 UTC
Permalink
Just bumping this to the top as it has the best discussion of html lazy
that I know of in this mailing list and Evan's latest talk (React Rally)
emphasised lazy there is such a way that I think I had better start using
it better
Hi Mark, feel free to hit me up on Slack sometime to discuss further. My
last two messages to this list have had more than one week delay in posting
(ignore the timestamp, I'm posting this on Friday July 21), so that might
be easier.
https://github.com/zwilias/elm-disco
I'm a firm believer that view functions should only receive data that they
directly interact with, and nothing more. I've just found that to be a much
better way to reason about interfaces.
Per the other thread discussing this this morning, I don't know that
passing the whole data model to all of the view-side functions is that
horrendous. (We pass around a UXConfig structure in all of our views to
provide access to translation functions amongst other things and the
biggest pain is that I'm not certain that making it he first parameter by
convention was the right choice.) Passing the whole data model feels icky,
but you can document the dependencies using different modules to access
different parts of the data model and you can still keep data model
mutation out of the views by emulating the command mechanism.
That said, there are, definitely friction points where being able to
subscribe to pieces of the data model would be a significant improvement.
See my other message for some examples of these. But the friction is not in
needing to pass the whole data model through the rendering process.
Mark
Post by Aaron VonderHaar
However, the downside of doing passing the entire model everywhere is
that you have to keep the entire model in your head when looking at any of
your view functions.
This is correct, but how can you avoid passing the whole model to all
views if the data model isn't structured similarly to your view hierarchy?
The idea of doing pre-processing on your data into a view-friendly
structure seems to add overhead and complexity, while also more or less
duplicating your view structure in two places: the view stack, and the view
data.
We have found that as apps grow in size, it is unreasonable to couple a
data model's internal structure to the requirements of a UI, and thus
passing The whole model is the only valid option for most view functions.
The exception are leaf nodes down the line that have few or no children, in
which case they can receive focused inputs only.
--
You received this message because you are subscribed to the Google Groups
"Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an
For more options, visit https://groups.google.com/d/optout.
--
You received this message because you are subscribed to a topic in the
Google Groups "Elm Discuss" group.
To unsubscribe from this topic, visit
https://groups.google.com/d/topic/elm-discuss/F5pdcsls1Zc/unsubscribe.
To unsubscribe from this group and all its topics, send an email to
For more options, visit https://groups.google.com/d/optout.
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Loading...