Elm with one message

597 views
Skip to first unread message

Vlad GURDIGA

unread,
Aug 24, 2017, 3:10:07 AM8/24/17
to Elm Discuss
Hey Elm-guys and Elm-gals! 👋

I have this toy-project where I’m getting my feet wet with Elm, and I’ve found an approach to compose view function that’s a bit different than what The Elm Architecture recommends. 🤓

Because I found message transformation (mapping) between nested components to be counterintuitive, I left out messages, for the most part. I only have one for the whole app: 🙃

type Msg
    = Update Model (Cmd Msg) (Sub Msg)


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Update model cmd sub ->
            ( { model | subscription = sub }, cmd )

Only the top component references it, and from then on I use callback-style functions for wiring up the nested components:

view : Dosar -> (Dosar -> Cmd msg -> Sub msg -> msg) -> Html msg
view (Dosar data) callback =
    let
        c data =
            callback (Dosar data)
    in
        div []
            [ h1 [] [ text "Dosar nou" ]
            , Temei.view data.temei (\v -> c { data | temei = v })
            , DocumentExecutoriu.view data.documentExecutoriu (\v -> c { data | documentExecutoriu = v } Cmd.none Sub.none)
            , Actiune.view data.actiune (\v -> c { data | actiune = v })
            ]


unlabeledTextField : String -> (String -> msg) -> List (Html msg)
unlabeledTextField defaultValue callback =
    [ input
        [ value defaultValue
        , onInput callback
        ]
        []
    ]

It seems to work well so far, and coming from JS I find this style a bit more familiar conceptually: I only have functions and values. 🤓

I am at about 184K of Elm now, and I’m wondering if anyone else have tried to do it this way and maybe found some gotchas that I should be aware of. In particular, with regards to how the code compiles to JS and maybe other Elm’s inner workings. 🤔

Cheers! 🤠

(NOTE: If you want to browse the code, please use this revision: https://github.com/gurdiga/xo.elm/tree/9fe9bd1. After that I’m bringing in elm-mdl, and the code will probably get too noisy for the purpose of this conversation. 🙂)

Peter Damoc

unread,
Aug 24, 2017, 3:47:24 AM8/24/17
to Elm Discuss
On of the key recommendations of Elm Architecture is to have your messages be just data. 

Your approach is somewhat similar to the approach of elm-sortable-table (except for the Cmds and Subs). 

Without  using Cmds and/or Subs it is an interesting approach if you use it for managing very small bits of state (the open/close status of a dropdown) but I'm afraid that it might bring performance problems if you use it for the entirety of the app. 
You basically have to computer all the possible future states of the app. 


--
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+unsubscribe@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.



--
There is NO FATE, we are the creators.
blog: http://damoc.ro/

Robin Heggelund Hansen

unread,
Aug 24, 2017, 4:31:20 AM8/24/17
to Elm Discuss
Won't this break the reactor/debugger? You can't really tell what sort of messages is being passed around in your application with this model, if you want to export message history, it would fail as well I believe.

I think a better way would be to not have nested components. Keep your entire message hierarchy, and model structure, flat.

Rupert Smith

unread,
Aug 24, 2017, 11:57:36 AM8/24/17
to Elm Discuss
On Thursday, August 24, 2017 at 8:47:24 AM UTC+1, Peter Damoc wrote:
Without  using Cmds and/or Subs it is an interesting approach if you use it for managing very small bits of state (the open/close status of a dropdown) but I'm afraid that it might bring performance problems if you use it for the entirety of the app. 
You basically have to computer all the possible future states of the app. 

Not so sure about that, as the callbacks seem to only be invoked in continuations?:

Mark Hamburg

unread,
Aug 24, 2017, 12:18:44 PM8/24/17
to elm-d...@googlegroups.com
Ignoring the commands and subscriptions part, I think this could be summarized as being built around having any of the things that would send a message instead send a new state. This eliminates the level of indirection of wrapping the desire to change the state into a message and then unwrapping the message to actually do the state change. It is entirely based on data and hence should be compatible with message recording for the debugger. On the other hand, the messages will lack semantic information. (One could work around that by adding a logging text field that was set when building the update message but ignored when applying it.)

Where this would seem to fall down is commands and subscriptions. Those are going to produce asynchronous results and would risk baking in a stale model state. This is particularly true for commands. Imagine, for example, wiring up a tick command that will increment a tick field in the model after five seconds. Since the command will be constructed to send the single "set the state" message and since the only model it will have access to is the one that existed when the command was constructed, it basically becomes a "reset the state to where it was five seconds ago" command. If subscriptions really do get rebuilt after every cycle of the update loop, then this isn't a problem for them but the closure construction for each subscription is an expense. If they don't get rebuilt — and the code in the initial posting suggests that instead we're caching a subscriptions value — then as with commands, it would seem easy to end up with code that resurrects stale states.

Mark
--
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.

Michael Jones

unread,
Aug 25, 2017, 4:42:55 AM8/25/17
to Elm Discuss
I can second Mark's point. At my company we had a little experiment where chunks of state where sent in the messages. Not the whole model but not a single unit either. It seemed really promising for a moment and quite clean but then we started getting bugs which were very hard to understand. Turned out to be exactly what Mark suggested. Between messages and commands we'd have multiple copies of the same chunk of state in flight, each based on some original value but each ignorant of the changes that the other were representing. The end result was to just get the last state and lose anything associated with other messages or commands happening around the same time.

We moved back to the standard strategy and all is well. 

Michael

Vlad GURDIGA

unread,
Aug 27, 2017, 4:15:17 AM8/27/17
to Elm Discuss
Hey Peter! 👋

Thank you for looking into this thing! 🤓


On Thursday, August 24, 2017 at 10:47:24 AM UTC+3, Peter Damoc wrote:
On of the key recommendations of Elm Architecture is to have your messages be just data. 

Your approach is somewhat similar to the approach of elm-sortable-table (except for the Cmds and Subs). 

Without  using Cmds and/or Subs it is an interesting approach if you use it for managing very small bits of state (the open/close status of a dropdown) but I'm afraid that it might bring performance problems if you use it for the entirety of the app. 

Regarding “very small bits” — I think this idea was one of the goals of going this way: any module represents one domain concept, and its view’s callback function deals with a value of that domain concept’s type. If a module refers to another domain concept, that concept has its separate module with its type, and view, and callback function that only knows about itself. (Well, ideally at least, for the most part, you know… 😉)
 
You basically have to computer all the possible future states of the app.

Not sure what you mean here. Could you please expand, or maybe point to an example in the code? 🤔 

Vlad GURDIGA

unread,
Aug 27, 2017, 4:16:08 AM8/27/17
to Elm Discuss
Hey Robin! 👋

Thank you for checking and thinking about my exploration here! 🙂


On Thursday, August 24, 2017 at 11:31:20 AM UTC+3, Robin Heggelund Hansen wrote:
Won't this break the reactor/debugger? You can't really tell what sort of messages is being passed around in your application with this model, if you want to export message history, it would fail as well I believe.

If by “break” you mean that I won’t be able to tell by looking at the message history in the Debugger window, then you’re right: there is only one kind of message, and you can’t really tell much just by looking at that. 😶

The reason it wasn’t an issue for me is that I didn’t use the Debugger and stopped using the Reactor as soon as I brought in ports. I don’t know how to plug in the JS that I call through ports, without having a separate index.html in which I plug the external librarythe bundle coming out of Elm, and then the bits of glue code to tie them together. 😑

I’m wondering if anyone has experience with having ports working with the Elm Reactor. 🤔
 
I think a better way would be to not have nested components. Keep your entire message hierarchy, and model structure, flat.

Yeah, this is the default recommendation that I hear in the Elm community. I’ve heard Richard Feldman mentioning that they also have a flat structure for their production app at NoRedInk, but I think I just can’t imagine how would that look like, and then, as I guess every other mortal, I fallback to ways that I know. 🙃

One other reason why I went hierarchical is that I’m used to seeing domain models as hierarchies, and in My Ideal World®✨ the code follows the domain as close as possible. 😎

I’m definitely open to hearing, or — even better — seeing examples of large apps that use a flat structure. 🤓

Vlad GURDIGA

unread,
Aug 27, 2017, 4:33:16 AM8/27/17
to Elm Discuss
Hey Mark! 👋

Thank you for taking time to look into this! 🤓


On Thursday, August 24, 2017 at 7:18:44 PM UTC+3, Mark Hamburg wrote:
Ignoring the commands and subscriptions part, I think this could be summarized as being built around having any of the things that would send a message instead send a new state. This eliminates the level of indirection of wrapping the desire to change the state into a message and then unwrapping the message to actually do the state change.

I haven’t though of it in such a well-formalized manner, but yes, that sounds like it: I eliminate one level of indirection. Or at least at some level, because at the very top there is still a message that triggers the update, even if it’s a super-generic one. 🤔
 
It is entirely based on data and hence should be compatible with message recording for the debugger.

This looks about right too, as I mentioned in my response to Robin, the debugger does as good of a job as it can with what I’m giving it. 👽
 
On the other hand, the messages will lack semantic information. (One could work around that by adding a logging text field that was set when building the update message but ignored when applying it.)

Yeah, that’s true. As I’ve mentioned, the reason why I haven’t sensed this problem is because I’m not using the Elm Reactor, because I don’t know how to get ports playing inside it. 😑
 
Where this would seem to fall down is commands and subscriptions. Those are going to produce asynchronous results and would risk baking in a stale model state. This is particularly true for commands. Imagine, for example, wiring up a tick command that will increment a tick field in the model after five seconds. Since the command will be constructed to send the single "set the state" message and since the only model it will have access to is the one that existed when the command was constructed, it basically becomes a "reset the state to where it was five seconds ago" command. If subscriptions really do get rebuilt after every cycle of the update loop, then this isn't a problem for them but the closure construction for each subscription is an expense. If they don't get rebuilt — and the code in the initial posting suggests that instead we're caching a subscriptions value — then as with commands, it would seem easy to end up with code that resurrects stale states.

Hm… I haven’t thought about this aspect of the commands+subscriptions duo, although I think I understand the mechanics. 🤔

So far I think I’m in luck: I have one port (a in/out couple actually) for talking to an external rich text editor, which I expect to be modal. The user clicks a button, the editor opens in a modal dialog, edit-edit-edit, then closes the editor and passes back to Elm, so it’s serialized which means there should not be any other changes to the Elm state while the editor is open. 😎

I’m guessing in time, there could come in more things like background sync-up of some kind, and then it’ll be really good to keep in mind these things that you’re mentioning about stale states. 🤓

Thank you! Really good catch! 👍

Vlad GURDIGA

unread,
Aug 27, 2017, 4:38:46 AM8/27/17
to Elm Discuss
Hey Michael! 👋

Thank you for chipping in! 🤓


On Friday, August 25, 2017 at 11:42:55 AM UTC+3, Michael Jones wrote:
I can second Mark's point. At my company we had a little experiment where chunks of state where sent in the messages. Not the whole model but not a single unit either. It seemed really promising for a moment and quite clean but then we started getting bugs which were very hard to understand. Turned out to be exactly what Mark suggested. Between messages and commands we'd have multiple copies of the same chunk of state in flight, each based on some original value but each ignorant of the changes that the other were representing. The end result was to just get the last state and lose anything associated with other messages or commands happening around the same time.

We moved back to the standard strategy and all is well. 

So — just to check my understanding — the standard strategy is to only send enough bits of data with the message to describe the change that the update function needed to carry on, an not the actual changed bits of state. Am I close? 🤔

Peter Damoc

unread,
Aug 27, 2017, 12:13:18 PM8/27/17
to Elm Discuss
On Sun, Aug 27, 2017 at 11:15 AM, Vlad GURDIGA <gur...@gmail.com> wrote:
You basically have to computer all the possible future states of the app.

Not sure what you mean here. Could you please expand, or maybe point to an example in the code? 🤔 

Let's simply the problem space and imagine that you have 3 buttons that when clicked do some serious computation on the model. 

With a traditional approach you have the message sent by one of the buttons and the specific computation done in response to the message. 

With this approach, you would have to compute all 3 heavy computation and send the new state with the message.  If your main message is: 

type Msg
    = Update Model

then the 3 messages you will have to use on `onClick` would be equivalent to:

div []
    [ button [onClick (Update (doTheUpdateForMessageOne oldModel))] [text "Do First Thing"] 
    , button [onClick (Update (doTheUpdateForMessageTwo oldModel))] [text "Do Second Thing"] 
    , button [onClick (Update (doTheUpdateForMessageThree oldModel))] [text "Do Third Thing"] 
    ]

Or, more realistically, inside a child component with its own `update`, have something like: 

view onUpdate model = 
    let 
        toMsg msg = onUpdate (update msg model) 
    in 
        div []
            [ button [onClick (toMsg First)] [text "Do First Thing"] 
            , button [onClick (toMsg Second)] [text "Do Second Thing"] 
            , button [onClick (toMsg Third)] [text "Do Third Thing"] 
            ]

this means that by the time the view is rendered, all 3 future states of the that component have been computed.
This is not a big issue if computing all future states is trivial (e.g. one toggle state of some drawer) so it's fine for small widgets but if you scale this to the level of the entire app, you might run into performance issues. 



Mark Hamburg

unread,
Aug 27, 2017, 9:01:20 PM8/27/17
to elm-d...@googlegroups.com
One other note: If you don't care about Reactor and the Debugger but do care about async problems, you could get almost the same structure by passing back Model -> Model functions instead of Model values.  Doing so may cause you to receive a visit from the Elm thought police but it is entirely semantically viable within Elm (and it's what I could imagine a lot of other functional languages doing by default).

Mark
--

Robin Heggelund Hansen

unread,
Aug 28, 2017, 9:28:03 AM8/28/17
to Elm Discuss
You can use the Elm debugger just fine with, say, webpack. Just pass in the `debugger: true` flag and you're good to go.

Vlad GURDIGA

unread,
Aug 31, 2017, 2:54:38 PM8/31/17
to Elm Discuss
Ooooh! …now I understand. 🤔

There was this question looping into the back of my mind regarding onClick callbacks: “How do I get a zero-argument function in Elm?” and your explanation clarifies it — there can’t be a zero-argument function in Elm! Right? 😶

I’m surely curious about the philosophical implication of this, but even without them I’m happy to have this clarified. 🤓

Thank you!

Vlad GURDIGA

unread,
Aug 31, 2017, 2:57:24 PM8/31/17
to Elm Discuss

On Monday, August 28, 2017 at 4:01:20 AM UTC+3, Mark Hamburg wrote:
One other note: If you don't care about Reactor and the Debugger but do care about async problems, you could get almost the same structure by passing back Model -> Model functions instead of Model values.

Sorry Mark, this sounds cool, but I’m kind of a slow thinker (generally) — could you give some pseudo-code to show an example of what you mean? 😊

Vlad GURDIGA

unread,
Aug 31, 2017, 3:01:12 PM8/31/17
to Elm Discuss
This would probably come in handy later, but for now I really enjoy having elm-make as my only build step. 🤓

Mark Hamburg

unread,
Aug 31, 2017, 3:29:18 PM8/31/17
to Elm Discuss
Your previous update structure was essentially (ignoring commands and subscriptions)

type Msg
    = Set Model

update : Msg -> Model -> Model
update (Set newModel) model =
    newModel

used as something like:

... onClick (Set { model | counter = model.counter + 1 }) ...

The revised approach looks like:

type Msg
    = Apply (Model -> Model)

update : Msg -> Model -> Model
update (Apply fn) model =
    fn model

used as:

... onClick (Apply (\model -> { model | counter = model.counter + 1 })) ...

The point is that we avoid capturing the model value in the message being routed around so that we can interleave this message with other asynchronous responses.

Again, as noted in my earlier message, this is not the Elm way and will be unfriendly to tools like the debugger but it is valid Elm. It is somewhat more complicated than the original version but still avoids the level of indirection associated with messages. It is also more efficient than the original form in that it at most constructs new function closures rather than constructing entire new model values for each possible action in the UX.

Mark

P.S. I was tempted to also include this form in the example:

... onClick (Apply incrementCounter) ...

incrementCounter : Model -> Model
incrementCounter model =
    { model | counter = model.counter + 1 }

But if one did that, one might as well have an IncrementCounter message and handle it in the update 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+unsubscribe@googlegroups.com.

Vlad GURDIGA

unread,
Sep 1, 2017, 2:38:31 AM9/1/17
to Elm Discuss
Oh wow! This looks super cool! 🤓

…and it seems this would also cover for the issue that Peter mentioned: this gives me the ability to have a real onClick callback without pre-computing the expected-after-click state! 🤠 — Right Peter? 🤔

OK, this is good. I’m not sure if I want to completely move to this model right away — although Elm makes it easy to refactor in any way imaginable 😉 — but it’s definitely useful to have it under my belt. 🤓

Thank you Mark! 🙂
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.

Peter Damoc

unread,
Sep 1, 2017, 2:45:17 AM9/1/17
to Elm Discuss
On Fri, Sep 1, 2017 at 9:38 AM, Vlad GURDIGA <gur...@gmail.com> wrote:
Oh wow! This looks super cool! 🤓

…and it seems this would also cover for the issue that Peter mentioned: this gives me the ability to have a real onClick callback without pre-computing the expected-after-click state! 🤠 — Right Peter? 🤔

As far as I understand things, this breaks the debugger because instead of data, the messages contain functions. 
The recommendation is to keep Models and Msg as pure data. 




Michael Jones

unread,
Sep 1, 2017, 4:08:25 AM9/1/17
to Elm Discuss
"The recommendation is to keep Models and Msg as pure data." 

Sound advice. I think that one trouble with passing functions around in messages is that as they get more complex you stand a bigger & bigger chance of closing over some other state and then you're back to the problem of passing stale chunks of state around in your messages and it would be enormously tricky to track down. 
  

Vlad GURDIGA

unread,
Sep 1, 2017, 5:35:04 AM9/1/17
to Elm Discuss
Hm… OK, noted. 🤔

Vlad GURDIGA

unread,
Sep 5, 2017, 3:02:29 AM9/5/17
to Elm Discuss
So… 🤔

I’m adding adding a couple of buttons this morning, and I feel I’m worried about that thing that Peter mentioned: Whenever I have an onClick with a callback instead of message I basically pre-compute the after-click state. I think I understand the issue, and I’d like to do it The Right Way®. 😐

In my app I have a large data entry form where initial fields determine which subsequent fields will be. It’s my understanding that every change to the model should be its own message. So far I have 50 fields in total (and there will probably be more) — does this mean that I need to have 50 things in my Msg? Or am I seeing it wrong? 🤓

Peter Damoc

unread,
Sep 5, 2017, 4:11:34 AM9/5/17
to Elm Discuss
On Tue, Sep 5, 2017 at 10:02 AM, Vlad GURDIGA <gur...@gmail.com> wrote:
In my app I have a large data entry form where initial fields determine which subsequent fields will be. It’s my understanding that every change to the model should be its own message. So far I have 50 fields in total (and there will probably be more) — does this mean that I need to have 50 things in my Msg? Or am I seeing it wrong? 🤓

Yes, you need to handle all 50 things in your Msg but you don't need to have all 50 tags in the main Msg. 

You can handle things in chunks. 
Here is the Forms example altered to show one way to approach this:
https://ellie-app.com/4dhSNYfnrqya1/0

Here is another way to structure that code:
https://ellie-app.com/4dhSNYfnrqya1/1

And yet another way to structure it:
https://ellie-app.com/4dhSNYfnrqya1/3

What might be appropriate to do in your case depends on the shape of the data. If the data can be split in smaller records that make sense maybe you should split it. 
If the data cannot be split and just needs to be one large record, then maybe you can split just the updates in some manner that makes sense to you and keep passing the same big record to all the update functions (second link from above) 

Needless to say that what you have above is only one section extracted. With multiple sections you will just have one root Msg for each section and update functions for each section.  

Vlad GURDIGA

unread,
Sep 6, 2017, 3:01:28 AM9/6/17
to Elm Discuss
Thank you Peter! 🤓

The third option that you proposed seems to fit nicer with the shape of my data: multiple nested records. I’ve tried it, and, I got something compiling! 😎 https://github.com/gurdiga/xo.elm/pull/1.

So now my child-module exposes additionally its Msg and its update function, which the parent-module uses to wire it up in its own fabric.

Does this look close to what you had in mind? 🤔

Vlad GURDIGA

unread,
Sep 9, 2017, 4:41:44 AM9/9/17
to Elm Discuss
Hey everybody!

Looking back on this thread, it looks like there are two major issues that were brought up: 🤔
  1. unintended state pre-computation in case of onClick handlers;
  2. accidental resurrection of the pieces of state captured in closures.
From my understanding, it seems like (2.) is not directly related to my mostly-messageless approach I initially presented here, and can also happen with the standard TEA, so it’s more like something to keep in mind than something that is immediately fixable in my code. Noted. ✍️

The (1.) though, seems directly related to my mostly-messageless approach. As Peter pointed out, it will get more problematic as the state grows, and I wanted to solve it. The solution that Peter proposed was to re-introduce messages and update functions, which would have solved the problem by moving the state computation to the update function, and so deferring it until the click actually happens, as appropriate per TEA. I’ve tried that (to best of my understanding), but compared with the mostly-messageless approach, it seems like the amount of indirection and code required by messages and update function does not justify itself. 🤔

One other thought that crossed my mind during this conversation — regarding the state pre-computation issue — was to have the onClick callback be an actual function that can be invoked when the click happens. The thing though, is that because onClick doesn’t have a value to yield, its callback is basically a zero-argument function, and so is evaluated immediately — which is actually the essence of the issue. 😐

I worked around that by re-implementing onClick to accept a one-argument function instead: 🤓

onClick : (String -> msg) -> Html.Attribute msg
onClick callback =
    Html.Events.on "click" (Json.Decode.map callback Html.Events.targetValue)

…even though the callback argument (an empty string) ends up ignored. 🤓

Problem solved! 😎 🙂

Mark Hamburg

unread,
Sep 9, 2017, 5:31:14 PM9/9/17
to Elm Discuss
If you are going to go that route, then you might as well just send the function to invoke back around through the message loop. Well, I guess that breaks the debugger in a way that sending the new model through the loop does not, but as noted previously sending the model has issues with async responses.

Mark

--
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+unsubscribe@googlegroups.com.
Reply all
Reply to author
Forward
0 new messages