ajs
2017-07-11 00:00:48 UTC
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
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.
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.