Discussion:
[elm-discuss] Separating model state structure from the view hierarchy
ajs
2017-07-11 00:00:48 UTC
Permalink
I had an interesting discussion with several members on the #beginners
channel on Slack. It was suggested I post this out to the larger community
for input.

As a quick background, I'm a professional Clojurescript developer for many
years, and have been through the early days of React (when it was just
wrapped as Om in Clojurescript), and then Reagent, and then the most
versatile and structured tool, Re-Frame, which has emerged as a leading
model of UI <-> Model interaction. I am now looking seriously at Elm on my
company's behalf and we are testing a prototype for a component in it.

The Elm Architecture is often compared to Redux and Re-Frame. The overall
flow is similar, however there is a particular problem that plagued
Clojurescript's Om in the early days that was eventually worked out in
Re-Frame and in later versions of Om. This problem, however, does not
appear to have a solution in Elm, and I wish to outline it here. It is a
common problem that occurs in modelling a UI in a complex SPA, anything
truly non-trivial.

The way the Elm Architecture works, and in the early Om, is that you have a
single piece of state that acts as the truth for your app, and then you
have view functions that receive parts of this state (or all of it), and
then they call children view functions and pass along parts of the state
they received. I have 2 Elm examples below, in a moment.

The fundamental problem with this approach is that it creates a tight
coupling between the organization of your model data and that of your view
hierarachy. What the Om users realized is that a robust model should not
depend on the views for its structure. But the Elm Architecture kind of
requires that (as far as I can tell). Data models should not be structured
based on what works for a user interface; they need to honor the
requirements of the data, only.

A very large and prominent Om-based user-interface is that which CircleCI
uses. They wrote about this issue here:

(skip to the heading "The Conundrum Of Cursors: Most Data Is Not A Tree.")
https://circleci.com/blog/why-we-use-om-and-why-were-excited-for-om-next/

If the model does not fit the needs of the view hierarchy, then what
emerged in the old Om was the unfortunate side effect of passing the entire
model state through all view functions. I hear that this is not uncommon in
Elm as well.

It has plagued many companies (including mine), which eventually led to a
much better way of data interacting with a UI in Re-Frame, which I will
summarize in a moment.

The problem arises when a view child needs data in the central model that
its parent did not need or receive. The parents and ancestors should not
require awareness of specific model details that a distant child might
need. For example, a view function might have the job of displaying Screen
A or Screen B. It is passed a flag, perhaps, that tells it which screen to
build, and then it calls a view function for that screen. Simple as that;
it doesn't need to know that Screen B has a widget that contains a dropdown
menu that must show a list of items from somewhere in the central model,
and that it must also find and pass that list to the screen view function.

The other problem that arises is when a child requires access to more than
one part of the central model, and those parts are not "in the same place".

One "solution": you pass the entire model state to all view functions, then
each view just takes what it needs. But this is very poor for a variety of
reasons, not the least of which is that children now have much more access
than they need. And it can lead to more code as each child has to go grab
what it needs from a large monolithic data source before operating it -- it
requires children to be responsible for both the query and the processing.
It makes it hard to trace data access through your app when all views
access the same global model. And it requires more code on behalf of
children.

Here are two Elm examples that show the problem:

Line 22 on:
https://ellie-app.com/3JbGH7v2v7ra1/1
How to give the child data that its parent didn't need or receive? Here,
I've hard-coded it to 0 because there is no other obvious way to access
what the child needs.

Another attempt:
https://ellie-app.com/3JbNQk26qNRa1/0
On Line 32, an ancestor is still having to hunt down data that a
potentially very distant child would need, and actually handle the building
of that child.

We have an app with hundreds, perhaps over a thousand, independent view
components, in a hierarchy about a hundred levels deep. This relationship
between data and components breaks down at that scale (and actually at
scales far smaller than ours) -- unless you can provide me with a technique
I haven't considered. It either leads to a lot of spaghetti code where
things have access to data they shouldn't and where components are handling
lots of intermediate data they don't directly use, or, it leads to
widespread use of global data for even the tiniest of detailed views.

This problem was solved in the Clojurescript community in two ways. I won't
discuss the Om Next method because it is still in alpha and not widely
adopted yet. Re-Frame, however, is extremely popular, and this is a summary
of how it works:

Just as you have a single central data store, you also have pure functions
that stand alone as queries to the data store. A query can be as simple as
pulling out a record value, or a deeply nested value, or the query function
might actually do some prep or calculation on required data and return
that. Doesn't matter, they just control the data flow to the views. These
functions are called "subscriptions". Each view function can subscribe to
any of these functions. This has at least 3 outstanding benefits: 1) each
child is truly a separate component and does not need to be aware of what
it's own children need, and doesn't impose dependencies on what its parent
needs to send it, and also 2) it places all read access to the db in one
place (well, assuming you put all these subscription functions in the same
file, which is common), and 3) it gives a children razor-sharp focus on
just exactly the data it needs to build itself, and nothing more. You
always know how the data is getting accessed by your views without actually
looking at your view code. It's not unlike how in Elm you always know what
possible Messages are getting sent, because they all eventually collapse to
one entry point. That's nice. I'd like to know how to do that in Elm for
reading data too, because it's very useful.

I should note that the way Re-Frame handles updates, or state-changing
messages, is essentially the same as Elm.

So it all boils down to the data a view needs to build itself, and how to
remove this burden on the view's ancestors.

I would really like some ideas/examples how to approach this problem in
Elm, if there is a clear solution.

Cheers,
Andrew
--
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.
Martin Norbäck Olivers
2017-07-21 15:29:07 UTC
Permalink
Hi!
We discussed this on the slack the other day, right?
Let me just get this straight, the Re-Frame solution depends on functions
having access to a global "db" object or similar (that corresponds to the
Elm model)?

How is that conceptually different than passing the model as a parameter
through the functions? Any function can access any part of the state by
subscribing in Re-Frame, just as it can access any part of the state by
accessing the passed-through model in Elm.

I just want to know what more this gives than avoiding the boilerplate of
passing the model through.
Btw, Elm has pure functions so implementing this straight-off is not
possible.

Regards,

Martin
Post by ajs
I had an interesting discussion with several members on the #beginners
channel on Slack. It was suggested I post this out to the larger community
for input.
As a quick background, I'm a professional Clojurescript developer for many
years, and have been through the early days of React (when it was just
wrapped as Om in Clojurescript), and then Reagent, and then the most
versatile and structured tool, Re-Frame, which has emerged as a leading
model of UI <-> Model interaction. I am now looking seriously at Elm on my
company's behalf and we are testing a prototype for a component in it.
The Elm Architecture is often compared to Redux and Re-Frame. The overall
flow is similar, however there is a particular problem that plagued
Clojurescript's Om in the early days that was eventually worked out in
Re-Frame and in later versions of Om. This problem, however, does not
appear to have a solution in Elm, and I wish to outline it here. It is a
common problem that occurs in modelling a UI in a complex SPA, anything
truly non-trivial.
The way the Elm Architecture works, and in the early Om, is that you have
a single piece of state that acts as the truth for your app, and then you
have view functions that receive parts of this state (or all of it), and
then they call children view functions and pass along parts of the state
they received. I have 2 Elm examples below, in a moment.
The fundamental problem with this approach is that it creates a tight
coupling between the organization of your model data and that of your view
hierarachy. What the Om users realized is that a robust model should not
depend on the views for its structure. But the Elm Architecture kind of
requires that (as far as I can tell). Data models should not be structured
based on what works for a user interface; they need to honor the
requirements of the data, only.
A very large and prominent Om-based user-interface is that which CircleCI
(skip to the heading "The Conundrum Of Cursors: Most Data Is Not A Tree.")
https://circleci.com/blog/why-we-use-om-and-why-were-excited-for-om-next/
If the model does not fit the needs of the view hierarchy, then what
emerged in the old Om was the unfortunate side effect of passing the entire
model state through all view functions. I hear that this is not uncommon in
Elm as well.
It has plagued many companies (including mine), which eventually led to a
much better way of data interacting with a UI in Re-Frame, which I will
summarize in a moment.
The problem arises when a view child needs data in the central model that
its parent did not need or receive. The parents and ancestors should not
require awareness of specific model details that a distant child might
need. For example, a view function might have the job of displaying Screen
A or Screen B. It is passed a flag, perhaps, that tells it which screen to
build, and then it calls a view function for that screen. Simple as that;
it doesn't need to know that Screen B has a widget that contains a dropdown
menu that must show a list of items from somewhere in the central model,
and that it must also find and pass that list to the screen view function.
The other problem that arises is when a child requires access to more than
one part of the central model, and those parts are not "in the same place".
One "solution": you pass the entire model state to all view functions,
then each view just takes what it needs. But this is very poor for a
variety of reasons, not the least of which is that children now have much
more access than they need. And it can lead to more code as each child has
to go grab what it needs from a large monolithic data source before
operating it -- it requires children to be responsible for both the query
and the processing. It makes it hard to trace data access through your app
when all views access the same global model. And it requires more code on
behalf of children.
https://ellie-app.com/3JbGH7v2v7ra1/1
How to give the child data that its parent didn't need or receive? Here,
I've hard-coded it to 0 because there is no other obvious way to access
what the child needs.
https://ellie-app.com/3JbNQk26qNRa1/0
On Line 32, an ancestor is still having to hunt down data that a
potentially very distant child would need, and actually handle the building
of that child.
We have an app with hundreds, perhaps over a thousand, independent view
components, in a hierarchy about a hundred levels deep. This relationship
between data and components breaks down at that scale (and actually at
scales far smaller than ours) -- unless you can provide me with a technique
I haven't considered. It either leads to a lot of spaghetti code where
things have access to data they shouldn't and where components are handling
lots of intermediate data they don't directly use, or, it leads to
widespread use of global data for even the tiniest of detailed views.
This problem was solved in the Clojurescript community in two ways. I
won't discuss the Om Next method because it is still in alpha and not
widely adopted yet. Re-Frame, however, is extremely popular, and this is a
Just as you have a single central data store, you also have pure functions
that stand alone as queries to the data store. A query can be as simple as
pulling out a record value, or a deeply nested value, or the query function
might actually do some prep or calculation on required data and return
that. Doesn't matter, they just control the data flow to the views. These
functions are called "subscriptions". Each view function can subscribe to
any of these functions. This has at least 3 outstanding benefits: 1) each
child is truly a separate component and does not need to be aware of what
it's own children need, and doesn't impose dependencies on what its parent
needs to send it, and also 2) it places all read access to the db in one
place (well, assuming you put all these subscription functions in the same
file, which is common), and 3) it gives a children razor-sharp focus on
just exactly the data it needs to build itself, and nothing more. You
always know how the data is getting accessed by your views without actually
looking at your view code. It's not unlike how in Elm you always know what
possible Messages are getting sent, because they all eventually collapse to
one entry point. That's nice. I'd like to know how to do that in Elm for
reading data too, because it's very useful.
I should note that the way Re-Frame handles updates, or state-changing
messages, is essentially the same as Elm.
So it all boils down to the data a view needs to build itself, and how to
remove this burden on the view's ancestors.
I would really like some ideas/examples how to approach this problem in
Elm, if there is a clear solution.
Cheers,
Andrew
--
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 16:43:12 UTC
Permalink
One argument against the "pass the model to all view functions" approach is
that it pretty much blows up laziness.

If one ignores that, then one could do something like the following:

* All view-side functions including both update and view receive a global
context containing the data model. (First argument? Last argument? I'm not
just throwing this out here. I'm looking for feedback on conventions
because I've had other places where a global context parameter has come
up.) This can be in addition to view-side model data as well such as which
element has focus.

* Commands get replaced on the view side with a construct that can embody
both operations on the outside world (i.e., traditional Elm commands) and
operations on the data model. The view side doesn't get write access to the
data model. The data model is modified in an update fold just as is normal
for Elm models.

The downsides to this approach include:

* It obscures the true dependencies between the data model and the views.
The compiler will catch changes (assuming they affect type signatures) but
the codebase may still feel hard to reason about. That said, one could
always provide multiple modules for interpreting the data model and one
could look at the dependices on those modules to reason about the
relationship of the views to the data model. In other words, it seems icky
but it might not be in practice.

* The view code can't react to changes in the data model — or at least
can't do so cheaply. This matters because the view code may want to do
things like change what it is focused on if an item in the data model goes
away. Or in the case of the code I'm working on, we have some expensive
layout logic that depends on he width of the view and the list of items
displayed in the view. We don't want to run this logic every time we render
so we need to know when either the width (a view-side property) or the item
list (a data-model property) changes. The non-cheap solution here is
broadcasting the new data model (or the old and the new data model) through
the view hierarchy on roughly every data model update.

Another solution would be to manage the data model via an effects manager
thereby allowing it to expose both commands for changes and subscriptions
for updates. This seems ideal for the scenario you've presented. The
problem is that effects managers cannot, AFAIK — please correct me if I'm
wrong, use other effects managers, so taking this approach cuts your data
model code off from the standard web sockets and HTTP implementations.

To get any deeper into Elm-based approaches, I think we would need to know
more about Re-Frame so any further details you can supply or point to would
be welcome.

Mark
Post by Martin Norbäck Olivers
Hi!
We discussed this on the slack the other day, right?
Let me just get this straight, the Re-Frame solution depends on functions
having access to a global "db" object or similar (that corresponds to the
Elm model)?
How is that conceptually different than passing the model as a parameter
through the functions? Any function can access any part of the state by
subscribing in Re-Frame, just as it can access any part of the state by
accessing the passed-through model in Elm.
I just want to know what more this gives than avoiding the boilerplate of
passing the model through.
Btw, Elm has pure functions so implementing this straight-off is not
possible.
Regards,
Martin
Post by ajs
I had an interesting discussion with several members on the #beginners
channel on Slack. It was suggested I post this out to the larger community
for input.
As a quick background, I'm a professional Clojurescript developer for
many years, and have been through the early days of React (when it was just
wrapped as Om in Clojurescript), and then Reagent, and then the most
versatile and structured tool, Re-Frame, which has emerged as a leading
model of UI <-> Model interaction. I am now looking seriously at Elm on my
company's behalf and we are testing a prototype for a component in it.
The Elm Architecture is often compared to Redux and Re-Frame. The overall
flow is similar, however there is a particular problem that plagued
Clojurescript's Om in the early days that was eventually worked out in
Re-Frame and in later versions of Om. This problem, however, does not
appear to have a solution in Elm, and I wish to outline it here. It is a
common problem that occurs in modelling a UI in a complex SPA, anything
truly non-trivial.
The way the Elm Architecture works, and in the early Om, is that you have
a single piece of state that acts as the truth for your app, and then you
have view functions that receive parts of this state (or all of it), and
then they call children view functions and pass along parts of the state
they received. I have 2 Elm examples below, in a moment.
The fundamental problem with this approach is that it creates a tight
coupling between the organization of your model data and that of your view
hierarachy. What the Om users realized is that a robust model should not
depend on the views for its structure. But the Elm Architecture kind of
requires that (as far as I can tell). Data models should not be structured
based on what works for a user interface; they need to honor the
requirements of the data, only.
A very large and prominent Om-based user-interface is that which CircleCI
(skip to the heading "The Conundrum Of Cursors: Most Data Is Not A Tree.")
https://circleci.com/blog/why-we-use-om-and-why-were-excited-for-om-next/
If the model does not fit the needs of the view hierarchy, then what
emerged in the old Om was the unfortunate side effect of passing the entire
model state through all view functions. I hear that this is not uncommon in
Elm as well.
It has plagued many companies (including mine), which eventually led to a
much better way of data interacting with a UI in Re-Frame, which I will
summarize in a moment.
The problem arises when a view child needs data in the central model that
its parent did not need or receive. The parents and ancestors should not
require awareness of specific model details that a distant child might
need. For example, a view function might have the job of displaying Screen
A or Screen B. It is passed a flag, perhaps, that tells it which screen to
build, and then it calls a view function for that screen. Simple as that;
it doesn't need to know that Screen B has a widget that contains a dropdown
menu that must show a list of items from somewhere in the central model,
and that it must also find and pass that list to the screen view function.
The other problem that arises is when a child requires access to more
than one part of the central model, and those parts are not "in the same
place".
One "solution": you pass the entire model state to all view functions,
then each view just takes what it needs. But this is very poor for a
variety of reasons, not the least of which is that children now have much
more access than they need. And it can lead to more code as each child has
to go grab what it needs from a large monolithic data source before
operating it -- it requires children to be responsible for both the query
and the processing. It makes it hard to trace data access through your app
when all views access the same global model. And it requires more code on
behalf of children.
https://ellie-app.com/3JbGH7v2v7ra1/1
How to give the child data that its parent didn't need or receive? Here,
I've hard-coded it to 0 because there is no other obvious way to access
what the child needs.
https://ellie-app.com/3JbNQk26qNRa1/0
On Line 32, an ancestor is still having to hunt down data that a
potentially very distant child would need, and actually handle the building
of that child.
We have an app with hundreds, perhaps over a thousand, independent view
components, in a hierarchy about a hundred levels deep. This relationship
between data and components breaks down at that scale (and actually at
scales far smaller than ours) -- unless you can provide me with a technique
I haven't considered. It either leads to a lot of spaghetti code where
things have access to data they shouldn't and where components are handling
lots of intermediate data they don't directly use, or, it leads to
widespread use of global data for even the tiniest of detailed views.
This problem was solved in the Clojurescript community in two ways. I
won't discuss the Om Next method because it is still in alpha and not
widely adopted yet. Re-Frame, however, is extremely popular, and this is a
Just as you have a single central data store, you also have pure
functions that stand alone as queries to the data store. A query can be as
simple as pulling out a record value, or a deeply nested value, or the
query function might actually do some prep or calculation on required data
and return that. Doesn't matter, they just control the data flow to the
views. These functions are called "subscriptions". Each view function can
subscribe to any of these functions. This has at least 3 outstanding
benefits: 1) each child is truly a separate component and does not need to
be aware of what it's own children need, and doesn't impose dependencies on
what its parent needs to send it, and also 2) it places all read access to
the db in one place (well, assuming you put all these subscription
functions in the same file, which is common), and 3) it gives a children
razor-sharp focus on just exactly the data it needs to build itself, and
nothing more. You always know how the data is getting accessed by your
views without actually looking at your view code. It's not unlike how in
Elm you always know what possible Messages are getting sent, because they
all eventually collapse to one entry point. That's nice. I'd like to know
how to do that in Elm for reading data too, because it's very useful.
I should note that the way Re-Frame handles updates, or state-changing
messages, is essentially the same as Elm.
So it all boils down to the data a view needs to build itself, and how to
remove this burden on the view's ancestors.
I would really like some ideas/examples how to approach this problem in
Elm, if there is a clear solution.
Cheers,
Andrew
--
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-07-21 19:29:08 UTC
Permalink
Confluence of threads between this one and the question about subscriptions
just made me realize that passing the data model down through the view
model hierarchy on each update to the data model is not the worst thing in
the world from a performance standpoint when you consider that a
subscription-based approach would be collecting up subscriptions from all
of the parts of the view model hierarchy and that would seem like a similar
amount of work unless one cached a lot of the subscriptions in the view
model for faster return.

So, that's good. On the other hand, another thing that subscriptions are
good for that this doesn't answer is as a way of expressing interest.
Imagine a data model with a list of chats and for each chat we have a list
of messages:

chats : DataModel -> Dict ChatID String -- chat id's and names

messages : DataModel -> ChatID -> List Message

We can pass the full data model around. The chat list view can extract the
list of chats. A message view for a particular chat can extract the list of
messages.

For efficiency reasons, we may only want to stay synchronized to the cloud
on the chat list and the current chat. Knowing which chat is current,
however, is really a view-side property. For example, the above API would
be just as friendly to having views into two chats at once. We could have
data model operations to start and stop following a chat. Starting is easy.
We just send the appropriate out message when we construct the view.
Stopping, however, is harder. One of the nice things about subscriptions is
that they go away when the piece of the model doing the subscribing goes
away. The functions we probably really want are something like:

getActiveChats : ViewModel -> Set ChatID

setActiveChats : Set ChatID -> DataModel -> DataModel


We need to call the former routine whenever the view model changes and use
the results to update the data model. Since that will change the data
model, we should then pass the updated data model to the view model causing
an update to the view model thereby looping us back around. To stop the
loop, we need to recognize when something hasn't changed and break the
cycle. That's easy enough but fragile since it would use an equality test
and equality isn't safe to use on arbitrary types. Effects managers don't
have this problem because we don't update the effects manager based on the
main model but rather the effects manager sends messages back to the main
model.

(I'm thinking about this a lot right now because I'm trying to move code to
an API where we need to request rendition URLs based on asset IDs over a
web socket where we are maintaining a synchronized set of assets of
interest between the cloud and the client. Coming up with a clean way to
organize the code is resulting in lots of exploratory sketches.)

Mark
Post by Mark Hamburg
One argument against the "pass the model to all view functions" approach
is that it pretty much blows up laziness.
* All view-side functions including both update and view receive a global
context containing the data model. (First argument? Last argument? I'm not
just throwing this out here. I'm looking for feedback on conventions
because I've had other places where a global context parameter has come
up.) This can be in addition to view-side model data as well such as which
element has focus.
* Commands get replaced on the view side with a construct that can embody
both operations on the outside world (i.e., traditional Elm commands) and
operations on the data model. The view side doesn't get write access to the
data model. The data model is modified in an update fold just as is normal
for Elm models.
* It obscures the true dependencies between the data model and the views.
The compiler will catch changes (assuming they affect type signatures) but
the codebase may still feel hard to reason about. That said, one could
always provide multiple modules for interpreting the data model and one
could look at the dependices on those modules to reason about the
relationship of the views to the data model. In other words, it seems icky
but it might not be in practice.
* The view code can't react to changes in the data model — or at least
can't do so cheaply. This matters because the view code may want to do
things like change what it is focused on if an item in the data model goes
away. Or in the case of the code I'm working on, we have some expensive
layout logic that depends on he width of the view and the list of items
displayed in the view. We don't want to run this logic every time we render
so we need to know when either the width (a view-side property) or the item
list (a data-model property) changes. The non-cheap solution here is
broadcasting the new data model (or the old and the new data model) through
the view hierarchy on roughly every data model update.
Another solution would be to manage the data model via an effects manager
thereby allowing it to expose both commands for changes and subscriptions
for updates. This seems ideal for the scenario you've presented. The
problem is that effects managers cannot, AFAIK — please correct me if I'm
wrong, use other effects managers, so taking this approach cuts your data
model code off from the standard web sockets and HTTP implementations.
To get any deeper into Elm-based approaches, I think we would need to know
more about Re-Frame so any further details you can supply or point to would
be welcome.
Mark
Post by Martin Norbäck Olivers
Hi!
We discussed this on the slack the other day, right?
Let me just get this straight, the Re-Frame solution depends on functions
having access to a global "db" object or similar (that corresponds to the
Elm model)?
How is that conceptually different than passing the model as a parameter
through the functions? Any function can access any part of the state by
subscribing in Re-Frame, just as it can access any part of the state by
accessing the passed-through model in Elm.
I just want to know what more this gives than avoiding the boilerplate of
passing the model through.
Btw, Elm has pure functions so implementing this straight-off is not
possible.
Regards,
Martin
Post by ajs
I had an interesting discussion with several members on the #beginners
channel on Slack. It was suggested I post this out to the larger community
for input.
As a quick background, I'm a professional Clojurescript developer for
many years, and have been through the early days of React (when it was just
wrapped as Om in Clojurescript), and then Reagent, and then the most
versatile and structured tool, Re-Frame, which has emerged as a leading
model of UI <-> Model interaction. I am now looking seriously at Elm on my
company's behalf and we are testing a prototype for a component in it.
The Elm Architecture is often compared to Redux and Re-Frame. The
overall flow is similar, however there is a particular problem that plagued
Clojurescript's Om in the early days that was eventually worked out in
Re-Frame and in later versions of Om. This problem, however, does not
appear to have a solution in Elm, and I wish to outline it here. It is a
common problem that occurs in modelling a UI in a complex SPA, anything
truly non-trivial.
The way the Elm Architecture works, and in the early Om, is that you
have a single piece of state that acts as the truth for your app, and then
you have view functions that receive parts of this state (or all of it),
and then they call children view functions and pass along parts of the
state they received. I have 2 Elm examples below, in a moment.
The fundamental problem with this approach is that it creates a tight
coupling between the organization of your model data and that of your view
hierarachy. What the Om users realized is that a robust model should not
depend on the views for its structure. But the Elm Architecture kind of
requires that (as far as I can tell). Data models should not be structured
based on what works for a user interface; they need to honor the
requirements of the data, only.
A very large and prominent Om-based user-interface is that which
(skip to the heading "The Conundrum Of Cursors: Most Data Is Not A Tree.")
https://circleci.com/blog/why-we-use-om-and-why-were-
excited-for-om-next/
If the model does not fit the needs of the view hierarchy, then what
emerged in the old Om was the unfortunate side effect of passing the entire
model state through all view functions. I hear that this is not uncommon in
Elm as well.
It has plagued many companies (including mine), which eventually led to
a much better way of data interacting with a UI in Re-Frame, which I will
summarize in a moment.
The problem arises when a view child needs data in the central model
that its parent did not need or receive. The parents and ancestors should
not require awareness of specific model details that a distant child might
need. For example, a view function might have the job of displaying Screen
A or Screen B. It is passed a flag, perhaps, that tells it which screen to
build, and then it calls a view function for that screen. Simple as that;
it doesn't need to know that Screen B has a widget that contains a dropdown
menu that must show a list of items from somewhere in the central model,
and that it must also find and pass that list to the screen view function.
The other problem that arises is when a child requires access to more
than one part of the central model, and those parts are not "in the same
place".
One "solution": you pass the entire model state to all view functions,
then each view just takes what it needs. But this is very poor for a
variety of reasons, not the least of which is that children now have much
more access than they need. And it can lead to more code as each child has
to go grab what it needs from a large monolithic data source before
operating it -- it requires children to be responsible for both the query
and the processing. It makes it hard to trace data access through your app
when all views access the same global model. And it requires more code on
behalf of children.
https://ellie-app.com/3JbGH7v2v7ra1/1
How to give the child data that its parent didn't need or receive? Here,
I've hard-coded it to 0 because there is no other obvious way to access
what the child needs.
https://ellie-app.com/3JbNQk26qNRa1/0
On Line 32, an ancestor is still having to hunt down data that a
potentially very distant child would need, and actually handle the building
of that child.
We have an app with hundreds, perhaps over a thousand, independent view
components, in a hierarchy about a hundred levels deep. This relationship
between data and components breaks down at that scale (and actually at
scales far smaller than ours) -- unless you can provide me with a technique
I haven't considered. It either leads to a lot of spaghetti code where
things have access to data they shouldn't and where components are handling
lots of intermediate data they don't directly use, or, it leads to
widespread use of global data for even the tiniest of detailed views.
This problem was solved in the Clojurescript community in two ways. I
won't discuss the Om Next method because it is still in alpha and not
widely adopted yet. Re-Frame, however, is extremely popular, and this is a
Just as you have a single central data store, you also have pure
functions that stand alone as queries to the data store. A query can be as
simple as pulling out a record value, or a deeply nested value, or the
query function might actually do some prep or calculation on required data
and return that. Doesn't matter, they just control the data flow to the
views. These functions are called "subscriptions". Each view function can
subscribe to any of these functions. This has at least 3 outstanding
benefits: 1) each child is truly a separate component and does not need to
be aware of what it's own children need, and doesn't impose dependencies on
what its parent needs to send it, and also 2) it places all read access to
the db in one place (well, assuming you put all these subscription
functions in the same file, which is common), and 3) it gives a children
razor-sharp focus on just exactly the data it needs to build itself, and
nothing more. You always know how the data is getting accessed by your
views without actually looking at your view code. It's not unlike how in
Elm you always know what possible Messages are getting sent, because they
all eventually collapse to one entry point. That's nice. I'd like to know
how to do that in Elm for reading data too, because it's very useful.
I should note that the way Re-Frame handles updates, or state-changing
messages, is essentially the same as Elm.
So it all boils down to the data a view needs to build itself, and how
to remove this burden on the view's ancestors.
I would really like some ideas/examples how to approach this problem in
Elm, if there is a clear solution.
Cheers,
Andrew
--
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.
Kasey Speakman
2017-07-24 18:12:15 UTC
Permalink
What you described is something that is common to do in event-sourced
systems. That is, each view maintains its own state and updates itself in
response to system events (aka left fold events to state). These are often
called "denormalizers" because the same data may be copied into multiple
views to avoid lookups. In this case, the data store would simply be a list
of all events that have occurred.

I have typically seen this done on the API, not on the UI, because
persistence is usually involved. But I suppose you could look at Model as
the resulting state from all UI events (aka Msg). But in Elm the events are
thrown away (usually <http://elm-lang.org/blog/the-perfect-bug-report>) and
only the resulting state remains.

You could make the Model just be a list of events -- `type alias Model =
List Msg` -- instead of expressing a particular state. Then you can left
fold events into whatever state you wanted. However, the longer the program
operates, the more events accumulate. At some point there will be too many
events to process in a reasonable time (or keep in memory). So to use an
accounting term, you will have to "close out" some number of old events.
That is, calculate the states based on them, and use those states as the
starting point for further processing. Then discard old events to save
space.

I don't know your problem domain, so I could not advise if this is an
appropriate solution... just an idea. Again, typically something seen on
the server-side. So not sure of other technical constraints you might face
in JS.
Post by ajs
I had an interesting discussion with several members on the #beginners
channel on Slack. It was suggested I post this out to the larger community
for input.
As a quick background, I'm a professional Clojurescript developer for many
years, and have been through the early days of React (when it was just
wrapped as Om in Clojurescript), and then Reagent, and then the most
versatile and structured tool, Re-Frame, which has emerged as a leading
model of UI <-> Model interaction. I am now looking seriously at Elm on my
company's behalf and we are testing a prototype for a component in it.
The Elm Architecture is often compared to Redux and Re-Frame. The overall
flow is similar, however there is a particular problem that plagued
Clojurescript's Om in the early days that was eventually worked out in
Re-Frame and in later versions of Om. This problem, however, does not
appear to have a solution in Elm, and I wish to outline it here. It is a
common problem that occurs in modelling a UI in a complex SPA, anything
truly non-trivial.
The way the Elm Architecture works, and in the early Om, is that you have
a single piece of state that acts as the truth for your app, and then you
have view functions that receive parts of this state (or all of it), and
then they call children view functions and pass along parts of the state
they received. I have 2 Elm examples below, in a moment.
The fundamental problem with this approach is that it creates a tight
coupling between the organization of your model data and that of your view
hierarachy. What the Om users realized is that a robust model should not
depend on the views for its structure. But the Elm Architecture kind of
requires that (as far as I can tell). Data models should not be structured
based on what works for a user interface; they need to honor the
requirements of the data, only.
A very large and prominent Om-based user-interface is that which CircleCI
(skip to the heading "The Conundrum Of Cursors: Most Data Is Not A Tree.")
https://circleci.com/blog/why-we-use-om-and-why-were-excited-for-om-next/
If the model does not fit the needs of the view hierarchy, then what
emerged in the old Om was the unfortunate side effect of passing the entire
model state through all view functions. I hear that this is not uncommon in
Elm as well.
It has plagued many companies (including mine), which eventually led to a
much better way of data interacting with a UI in Re-Frame, which I will
summarize in a moment.
The problem arises when a view child needs data in the central model that
its parent did not need or receive. The parents and ancestors should not
require awareness of specific model details that a distant child might
need. For example, a view function might have the job of displaying Screen
A or Screen B. It is passed a flag, perhaps, that tells it which screen to
build, and then it calls a view function for that screen. Simple as that;
it doesn't need to know that Screen B has a widget that contains a dropdown
menu that must show a list of items from somewhere in the central model,
and that it must also find and pass that list to the screen view function.
The other problem that arises is when a child requires access to more than
one part of the central model, and those parts are not "in the same place".
One "solution": you pass the entire model state to all view functions,
then each view just takes what it needs. But this is very poor for a
variety of reasons, not the least of which is that children now have much
more access than they need. And it can lead to more code as each child has
to go grab what it needs from a large monolithic data source before
operating it -- it requires children to be responsible for both the query
and the processing. It makes it hard to trace data access through your app
when all views access the same global model. And it requires more code on
behalf of children.
https://ellie-app.com/3JbGH7v2v7ra1/1
How to give the child data that its parent didn't need or receive? Here,
I've hard-coded it to 0 because there is no other obvious way to access
what the child needs.
https://ellie-app.com/3JbNQk26qNRa1/0
On Line 32, an ancestor is still having to hunt down data that a
potentially very distant child would need, and actually handle the building
of that child.
We have an app with hundreds, perhaps over a thousand, independent view
components, in a hierarchy about a hundred levels deep. This relationship
between data and components breaks down at that scale (and actually at
scales far smaller than ours) -- unless you can provide me with a technique
I haven't considered. It either leads to a lot of spaghetti code where
things have access to data they shouldn't and where components are handling
lots of intermediate data they don't directly use, or, it leads to
widespread use of global data for even the tiniest of detailed views.
This problem was solved in the Clojurescript community in two ways. I
won't discuss the Om Next method because it is still in alpha and not
widely adopted yet. Re-Frame, however, is extremely popular, and this is a
Just as you have a single central data store, you also have pure functions
that stand alone as queries to the data store. A query can be as simple as
pulling out a record value, or a deeply nested value, or the query function
might actually do some prep or calculation on required data and return
that. Doesn't matter, they just control the data flow to the views. These
functions are called "subscriptions". Each view function can subscribe to
any of these functions. This has at least 3 outstanding benefits: 1) each
child is truly a separate component and does not need to be aware of what
it's own children need, and doesn't impose dependencies on what its parent
needs to send it, and also 2) it places all read access to the db in one
place (well, assuming you put all these subscription functions in the same
file, which is common), and 3) it gives a children razor-sharp focus on
just exactly the data it needs to build itself, and nothing more. You
always know how the data is getting accessed by your views without actually
looking at your view code. It's not unlike how in Elm you always know what
possible Messages are getting sent, because they all eventually collapse to
one entry point. That's nice. I'd like to know how to do that in Elm for
reading data too, because it's very useful.
I should note that the way Re-Frame handles updates, or state-changing
messages, is essentially the same as Elm.
So it all boils down to the data a view needs to build itself, and how to
remove this burden on the view's ancestors.
I would really like some ideas/examples how to approach this problem in
Elm, if there is a clear solution.
Cheers,
Andrew
--
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-07-24 22:02:00 UTC
Permalink
I think your "another attempt" ( https://ellie-app.com/3JbNQk26qNRa1/0 ) is
a good way to decouple the parent from the data that the child needs. It
also seems appropriate for your example of the "parent" being concerned
only with layout, so it makes sense for it to have an API that allows it to
work with any arbitrary Html.

In cases where the "parent" is not simply generic layout, I think it is
appropriate for the parent to require certain data so that it can pass it
down. Having an explicit path for the data can make the nested view
hierarchy easier to learn and easier to refactor if data requirements later
change.

Ultimately, your question seems to be "what is the best way to port an app
with thousands of nested, stateful components to Elm?" I think the answer
to that is that if you do adopt Elm for that project, you should be
changing the structure of your app not to have thousands of nested,
stateful components. The Elm architecture generally pushes you away from
having many small components with state, and instead having a flatter
overall structure and reusable view modules that are stateless so that you
can cleanly manage your state at the top level. (Perhaps other can chime
in with links to some of the articles articles and discussions that talk
about that approach?)

In any case, I think you would not want to port your existing architecture
directly to Elm if you chose to adopt it for that project. You'd be better
off trying Elm on a new project, or selecting a small piece of your current
app to convert to Elm and use a new architecture within that piece.
Post by ajs
I had an interesting discussion with several members on the #beginners
channel on Slack. It was suggested I post this out to the larger community
for input.
As a quick background, I'm a professional Clojurescript developer for many
years, and have been through the early days of React (when it was just
wrapped as Om in Clojurescript), and then Reagent, and then the most
versatile and structured tool, Re-Frame, which has emerged as a leading
model of UI <-> Model interaction. I am now looking seriously at Elm on my
company's behalf and we are testing a prototype for a component in it.
The Elm Architecture is often compared to Redux and Re-Frame. The overall
flow is similar, however there is a particular problem that plagued
Clojurescript's Om in the early days that was eventually worked out in
Re-Frame and in later versions of Om. This problem, however, does not
appear to have a solution in Elm, and I wish to outline it here. It is a
common problem that occurs in modelling a UI in a complex SPA, anything
truly non-trivial.
The way the Elm Architecture works, and in the early Om, is that you have
a single piece of state that acts as the truth for your app, and then you
have view functions that receive parts of this state (or all of it), and
then they call children view functions and pass along parts of the state
they received. I have 2 Elm examples below, in a moment.
The fundamental problem with this approach is that it creates a tight
coupling between the organization of your model data and that of your view
hierarachy. What the Om users realized is that a robust model should not
depend on the views for its structure. But the Elm Architecture kind of
requires that (as far as I can tell). Data models should not be structured
based on what works for a user interface; they need to honor the
requirements of the data, only.
A very large and prominent Om-based user-interface is that which CircleCI
(skip to the heading "The Conundrum Of Cursors: Most Data Is Not A Tree.")
https://circleci.com/blog/why-we-use-om-and-why-were-excited-for-om-next/
If the model does not fit the needs of the view hierarchy, then what
emerged in the old Om was the unfortunate side effect of passing the entire
model state through all view functions. I hear that this is not uncommon in
Elm as well.
It has plagued many companies (including mine), which eventually led to a
much better way of data interacting with a UI in Re-Frame, which I will
summarize in a moment.
The problem arises when a view child needs data in the central model that
its parent did not need or receive. The parents and ancestors should not
require awareness of specific model details that a distant child might
need. For example, a view function might have the job of displaying Screen
A or Screen B. It is passed a flag, perhaps, that tells it which screen to
build, and then it calls a view function for that screen. Simple as that;
it doesn't need to know that Screen B has a widget that contains a dropdown
menu that must show a list of items from somewhere in the central model,
and that it must also find and pass that list to the screen view function.
The other problem that arises is when a child requires access to more than
one part of the central model, and those parts are not "in the same place".
One "solution": you pass the entire model state to all view functions,
then each view just takes what it needs. But this is very poor for a
variety of reasons, not the least of which is that children now have much
more access than they need. And it can lead to more code as each child has
to go grab what it needs from a large monolithic data source before
operating it -- it requires children to be responsible for both the query
and the processing. It makes it hard to trace data access through your app
when all views access the same global model. And it requires more code on
behalf of children.
https://ellie-app.com/3JbGH7v2v7ra1/1
How to give the child data that its parent didn't need or receive? Here,
I've hard-coded it to 0 because there is no other obvious way to access
what the child needs.
https://ellie-app.com/3JbNQk26qNRa1/0
On Line 32, an ancestor is still having to hunt down data that a
potentially very distant child would need, and actually handle the building
of that child.
We have an app with hundreds, perhaps over a thousand, independent view
components, in a hierarchy about a hundred levels deep. This relationship
between data and components breaks down at that scale (and actually at
scales far smaller than ours) -- unless you can provide me with a technique
I haven't considered. It either leads to a lot of spaghetti code where
things have access to data they shouldn't and where components are handling
lots of intermediate data they don't directly use, or, it leads to
widespread use of global data for even the tiniest of detailed views.
This problem was solved in the Clojurescript community in two ways. I
won't discuss the Om Next method because it is still in alpha and not
widely adopted yet. Re-Frame, however, is extremely popular, and this is a
Just as you have a single central data store, you also have pure functions
that stand alone as queries to the data store. A query can be as simple as
pulling out a record value, or a deeply nested value, or the query function
might actually do some prep or calculation on required data and return
that. Doesn't matter, they just control the data flow to the views. These
functions are called "subscriptions". Each view function can subscribe to
any of these functions. This has at least 3 outstanding benefits: 1) each
child is truly a separate component and does not need to be aware of what
it's own children need, and doesn't impose dependencies on what its parent
needs to send it, and also 2) it places all read access to the db in one
place (well, assuming you put all these subscription functions in the same
file, which is common), and 3) it gives a children razor-sharp focus on
just exactly the data it needs to build itself, and nothing more. You
always know how the data is getting accessed by your views without actually
looking at your view code. It's not unlike how in Elm you always know what
possible Messages are getting sent, because they all eventually collapse to
one entry point. That's nice. I'd like to know how to do that in Elm for
reading data too, because it's very useful.
I should note that the way Re-Frame handles updates, or state-changing
messages, is essentially the same as Elm.
So it all boils down to the data a view needs to build itself, and how to
remove this burden on the view's ancestors.
I would really like some ideas/examples how to approach this problem in
Elm, if there is a clear solution.
Cheers,
Andrew
--
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-21 14:05:58 UTC
Permalink
I got plenty of discussion on this in Slack while this was awaiting
moderation for a week or so; I don't think there is need to get into it
again. I'm comfortable with the feedback I got there. Thanks for all the
input and (spirited) talks about this! :)
Post by ajs
I had an interesting discussion with several members on the #beginners
channel on Slack. It was suggested I post this out to the larger community
for input.
As a quick background, I'm a professional Clojurescript developer for many
years, and have been through the early days of React (when it was just
wrapped as Om in Clojurescript), and then Reagent, and then the most
versatile and structured tool, Re-Frame, which has emerged as a leading
model of UI <-> Model interaction. I am now looking seriously at Elm on my
company's behalf and we are testing a prototype for a component in it.
The Elm Architecture is often compared to Redux and Re-Frame. The overall
flow is similar, however there is a particular problem that plagued
Clojurescript's Om in the early days that was eventually worked out in
Re-Frame and in later versions of Om. This problem, however, does not
appear to have a solution in Elm, and I wish to outline it here. It is a
common problem that occurs in modelling a UI in a complex SPA, anything
truly non-trivial.
The way the Elm Architecture works, and in the early Om, is that you have
a single piece of state that acts as the truth for your app, and then you
have view functions that receive parts of this state (or all of it), and
then they call children view functions and pass along parts of the state
they received. I have 2 Elm examples below, in a moment.
The fundamental problem with this approach is that it creates a tight
coupling between the organization of your model data and that of your view
hierarachy. What the Om users realized is that a robust model should not
depend on the views for its structure. But the Elm Architecture kind of
requires that (as far as I can tell). Data models should not be structured
based on what works for a user interface; they need to honor the
requirements of the data, only.
A very large and prominent Om-based user-interface is that which CircleCI
(skip to the heading "The Conundrum Of Cursors: Most Data Is Not A Tree.")
https://circleci.com/blog/why-we-use-om-and-why-were-excited-for-om-next/
If the model does not fit the needs of the view hierarchy, then what
emerged in the old Om was the unfortunate side effect of passing the entire
model state through all view functions. I hear that this is not uncommon in
Elm as well.
It has plagued many companies (including mine), which eventually led to a
much better way of data interacting with a UI in Re-Frame, which I will
summarize in a moment.
The problem arises when a view child needs data in the central model that
its parent did not need or receive. The parents and ancestors should not
require awareness of specific model details that a distant child might
need. For example, a view function might have the job of displaying Screen
A or Screen B. It is passed a flag, perhaps, that tells it which screen to
build, and then it calls a view function for that screen. Simple as that;
it doesn't need to know that Screen B has a widget that contains a dropdown
menu that must show a list of items from somewhere in the central model,
and that it must also find and pass that list to the screen view function.
The other problem that arises is when a child requires access to more than
one part of the central model, and those parts are not "in the same place".
One "solution": you pass the entire model state to all view functions,
then each view just takes what it needs. But this is very poor for a
variety of reasons, not the least of which is that children now have much
more access than they need. And it can lead to more code as each child has
to go grab what it needs from a large monolithic data source before
operating it -- it requires children to be responsible for both the query
and the processing. It makes it hard to trace data access through your app
when all views access the same global model. And it requires more code on
behalf of children.
https://ellie-app.com/3JbGH7v2v7ra1/1
How to give the child data that its parent didn't need or receive? Here,
I've hard-coded it to 0 because there is no other obvious way to access
what the child needs.
https://ellie-app.com/3JbNQk26qNRa1/0
On Line 32, an ancestor is still having to hunt down data that a
potentially very distant child would need, and actually handle the building
of that child.
We have an app with hundreds, perhaps over a thousand, independent view
components, in a hierarchy about a hundred levels deep. This relationship
between data and components breaks down at that scale (and actually at
scales far smaller than ours) -- unless you can provide me with a technique
I haven't considered. It either leads to a lot of spaghetti code where
things have access to data they shouldn't and where components are handling
lots of intermediate data they don't directly use, or, it leads to
widespread use of global data for even the tiniest of detailed views.
This problem was solved in the Clojurescript community in two ways. I
won't discuss the Om Next method because it is still in alpha and not
widely adopted yet. Re-Frame, however, is extremely popular, and this is a
Just as you have a single central data store, you also have pure functions
that stand alone as queries to the data store. A query can be as simple as
pulling out a record value, or a deeply nested value, or the query function
might actually do some prep or calculation on required data and return
that. Doesn't matter, they just control the data flow to the views. These
functions are called "subscriptions". Each view function can subscribe to
any of these functions. This has at least 3 outstanding benefits: 1) each
child is truly a separate component and does not need to be aware of what
it's own children need, and doesn't impose dependencies on what its parent
needs to send it, and also 2) it places all read access to the db in one
place (well, assuming you put all these subscription functions in the same
file, which is common), and 3) it gives a children razor-sharp focus on
just exactly the data it needs to build itself, and nothing more. You
always know how the data is getting accessed by your views without actually
looking at your view code. It's not unlike how in Elm you always know what
possible Messages are getting sent, because they all eventually collapse to
one entry point. That's nice. I'd like to know how to do that in Elm for
reading data too, because it's very useful.
I should note that the way Re-Frame handles updates, or state-changing
messages, is essentially the same as Elm.
So it all boils down to the data a view needs to build itself, and how to
remove this burden on the view's ancestors.
I would really like some ideas/examples how to approach this problem in
Elm, if there is a clear solution.
Cheers,
Andrew
--
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...