Batching updates


#1

When sending a particular msg to update, I need several things to update that all have their own msgs independently. I see two ways (I’m sure there are more) to accomplish this:
Option 1:

update : Msg -> Model -> Model
update msg model =
   Foo strng -> 
       let 
           mdl = { model | foo = strng }
      in
         update (Bar "barred") mdl
  Bar strng -> 
      { model | bar = strng }

Option 2:

update : Msg -> Model -> (Model, Cmd msg)
update msg model =
    case msg of 
        FooBar strng1 strng2-> (model, Cmd.batch 
                                           [ Task.perform (\_ -> Foo strng1) (Task.succeed True)
                                           , Task.perform (\_ -> Bar strng2) (Task.succeed True)
                                           ] )

       Foo strng -> ({ model | foo = strng }, Cmd.none)

       Bar strng -> ({ model | bar = strng }, Cmd.none)

Is one preferable to the other? Using Task.perform in this way seems odd but I don’t know if it’s wrong.


#2

You might also want to take a look at update-extra for some syntactic sugar.

https://package.elm-lang.org/packages/ccapndave/elm-update-extra/4.0.0/Update-Extra#sequence


#3

Thanks! That looks really handy. In the source it seems that the sequence function ultimately boils down a list of msgs into a sequentially updated model with the Cmds not processed immediately but instead gathered into a list that is processed ultimately as Cmd.batch [commands]. Am I misreading that? If not, is that preferable to either of the two approaches I laid out?


#4

I’ve seen this question come up several times and your Option 1 is usually what’s recommended.

With option 2 you’re unnecessarily making part of the update asynchronous. Looks safe in your simplified example but in more complex cases it might even be possible for another message to come in between your two updates. (Say a user click at the wrong time)

I believe you’re safe with the way you’re putting both updates in a single Cmd.batch here, but in real apps there are lots of ways to accidentally make the updates so they’re not guaranteed to happen together.

On the other hand option 1 makes it synchronous. One event, one message, one update. (Event meaning user click or http received or something)


#5

Yep, Option 1 is better than Option2.

But even better is to do:

doBar : String -> Model -> Model
doBar strng model = { model | bar = strng }

update : Msg -> Model -> Model
update msg model =
   Foo strng -> 
     let 
       mdl = { model | foo = strng }
     in
       doBar "barred" mdl
  Bar strng -> 
    doBar strng model

#6

Could refactor also doFoo into a function:

doFoo : String -> Model -> Model
doFoo strng model = { model | foo = strng }

doBar : String -> Model -> Model
doBar strng model = { model | bar = strng }

update : Msg -> Model -> Model
update msg model =
  case msg of
    Foo strng -> 
      model
        |> doFoo strng
        |> doBar "barred"
    Bar strng -> 
      model
        |> doBar strng

#7

Separate functions (as pointed out by @jessta and @malaire) are probably the way to go. If you don’t make them into separate functions, then you introduce an implicit dependency: when you receive a Foo, a Bar will occur via the recursive call, but so will any other logic (e.g. analytics) that also happen to be part of your update function. Of course, there is also the danger that as your update grows, you forget that Foo has an implicit dependency on Bar, and when you change Bar, you wonder why Foo isn’t working properly any more. Splitting things into separate functions will avoid both of these issues.

Lastly, consider splitting things up into different modules if it all gets too big. Then you can simply say

update : Msg -> Model -> (Model, Cmd msg)
update msg model =
   case msg of
      Foo fooMsg ->
         Foo.update fooMsg

… and so on. My guideline is that programming isn’t just for the computer: if it was, we’d all use assembly and be happy with it. Mostly, programming is for the programmer, since humans are so bad at keeping track of state explosions! So if you see an opportunity to split things up decently with very little performance impact, then go for it with a grin on your face and a song in your heart.


#8

In my actual project I do have things split into modules but I have a weird case where I need to do both Foo.update and Bar.update upon receipt of a msg.


#9

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.