Thoughts on an update wrapper?

I sometimes find myself in a situation where 99% of the time I don’t require a Cmd msg, but for one or two cases I do. So I end up writing code like this

type Msg
  = M1
  | M2
  | M3
  -- ...
  | Refresh

update : Msg -> Model -> ( Model, Cmd msg )
update msg model =
  case msg of
    M1 ->
      -- ...
      ( newModel, Cmd.none )
    M2 ->
      -- ...
      ( newModel, Cmd.none )
    M3 ->
      -- ...
      ( newModel, Cmd.none )
    -- ...
    Refresh ->
      -- ...
      ( newModel, oneOfTheOnlyCommandsIUse )

The tuples just feel awkward to me, so I tried this with a lambda that unwraps it in my call to Browser.application:

type Update model msg = NewModel model | WithEffect model msg

update : Msg -> Model -> Update Model Msg 
update msg model =
  case msg of
    M1 ->
      -- ...
      NewModel newModel
    M2 ->
      -- ...
      NewModel newModel
    M3 ->
      -- ...
      NewModel newModel
    -- ...
    Refresh ->
      -- ...
      WithEffect newModel oneOfTheOnlyCommandsIUse

To me this shines with record updates.

NewModel { model | someField = newValue }
-- vs
( { model | someField = newValue }, Cmd.none )

Technically, it’s about as repetitive as before, but to me it just feels nicer to me. What do you all think? Little quality of life improvement, ugly, not necessary, have something better?

3 Likes

This is a fairly common approach from what I’ve seen. We use it at work and I use it in my side projects. There’s a handful of packages for this as well, e.g. elm-update-helper 2.3.0, elm-update-pipeline 1.3.2, return 1.0.3, and a handful more if you search for “update”.

1 Like

Yeah, I’ve been in the same boat for a lot of my apps, but I usually just carry around a helper library with stuff like this:

done : a -> ( a, Cmd msg )
done =
    flip Tuple.pair Cmd.none

Then I can just do:

update : Msg -> Model -> Update Model Msg 
update msg model =
  case msg of
    M1 ->
      -- ...
      done newModel
    M2 ->
      -- ...
      newModel |> someFn |> done
  -- ... etc.

(I also have do for when I only need to run commands and don’t need any model changes).

Feels more composable and less noisy than a separate type, but, whatever works for ya. :grin:

2 Likes

Does anyone have/use a variant where they can specify something like NoModelChange, without having to pass in the original Model? Example:

type Update model msg
   = NoChange
   | NewModel model
   | Effect msg
   | NewModelAndEffect model msg


update : Msg -> Model -> Update Model Msg 
update msg model =
  case msg of
    Increment ->
      NewModel (increment model)

    TextFieldReceivedFocus ->
      NoChange

    UserClickedSaveButton ->
      let
          newModel : Model
          newModel =
             { model | httpRequestInProgress = True }
      in
      NewModelAndEffect (sendHttpRequest model)

    SaveFinished ->
      NewModel { model | httpRequestInProgress = False }

A nice benefit of this is that if the model doesn’t change when we use NoChange, then we don’t need to update the model either in the “parent” module, which can be beneficial to avoid unnecessary HTML lazy failures.

1 Like

Same with your Effect msg variant, since that also does not change the model?

We used to have helper functions like these to avoid writing the tuples. But it just introduced more cognitive load. So we removed them and just use (model, cmd) all the time.

3 Likes

Yes, absolutely. I’ve been thinking about this pattern for elm-review visitors, with performance in mind (but that’s more of a problem for rules than for update calls).

Yes, the simplicity and familiarity of the simple tuple is a big selling point.

Also, let’s not forget that not having a dedicated type means you are quite free with the return value: Have an update function that only returns Model, or return an “out msg” as well, …

1 Like

I find it useful to stick to the exact same return pattern everywhere:

updateLikeFn : … → Model → (Model, Cmd Msg)

And then use the update-helper functions to map, andThen and so on:

map : (a -> b) -> ( a, Cmd msg ) -> ( b, Cmd msg )
andThen : (model -> ( model2, Cmd msg ))
    -> ( model, Cmd msg )
    -> ( model2, Cmd msg )
pure : model -> ( model, Cmd msg )

pure is just the no-op case.

case msg of
    ...

    _ -> 
        pure model

The reason I like to stick with the 2-tuple is that it keeps the shape of the code regular, and inside the update case statements you get pipeline style code which is quite readable. I also dislike the out message pattern because of the danger of falling into the actor model message passing way of thinking about Elm modules, which I increasingly think is not a good pattern. Finally, it saves juggling between 2-tuple and 3-tuple or model-only forms which is a bit awkward.

That said, I am pretty sure the pure, map and andThen functions could be re-written to work over this proposed Update return type.

BTW, In this type do you mean the msg parameter to be an effect? That is, with some code elsewhere that will translate that into a Cmd?

The reason I ask is that not every Cmd msg can be encoded as its unwrapped msg.

type Msg
    = MyCustomMsg
    | RandInt
    | ...

-- produces Cmd Msg from Msg that simply wraps the Msg as an event
Task.perform identity (Task.succeed MyCustomMsg) 

-- produces Cmd Msg from Msg that performs the specific side effect of generating a random int.
-- no way to represent that particular Cmd Msg as (Effect msg)
Random.generate RandInt Random.int -- produces Cmd Msg

Or do we actually mean this?

type Update model msg
   = NoChange
   | NewModel model
   | Effect (Cmd msg)
   | NewModelAndEffect model (Cmd msg)

I think the thing I like about wrapping the ( model, Cmd msg ) can be best demonstrated with the difference between

let
    nextModel = { model | foo = someFn model }

    nextNextModel = { nextModel | bar = otherFn nextModel }
in
( { nextNextModel | baz = lastFn nextNextModel }, Cmd.none )

vs

{ model | foo = someFn model }
    |> Update.save
    |> Update.map otherFn
    |> Update.map lastFn

The first approach can be very bug prone whereas the second approach feels much more clear (to me) about what the steps are and the order of them.

1 Like

Yes, and it can be nice to read code like that too, if you choose good names for your functions. For example I have code like this:

    ( model, Cmd.none )
        |> andThen (moveCursorRowBy 1 pos)
        |> andThen scrollIfNecessary
        |> andThen calcViewableRegion
        |> andThen rippleBuffer
        |> andThen activity
1 Like

Discussing this at work and it was pointed out that you can also do

( model, Cmd.none )
        |> scrollIfNecessary
        |> Tuple.mapFirst activity

scrollIfNecessary : ( model, Cmd msg ) -> ( model, Cmd msg )

activity : model -> model
-- etc
2 Likes

Yes, this is more what I had in mind.

Maybe some advantage in the original though:

type Update model e
   = NoChange
   | NewModel model
   | Effect e
   | NewModelAndEffect model e

-- Throughout the application:

update : Model -> Msg -> Update Model (Effect Msg)

-- Then at the effect translation in the top-level

effectToCmd : Update Model (Effect Msg) -> Update Model (Cmd Msg)

Sure that works, but as I say, I like to keep all my update-like functions in the same pattern, so:

scrollIfNecessary : Model -> ( Model, Cmd Msg )
activity : Model -> ( Model, Cmd Msg )
whatever : ... -> Model -> ( Model, Cmd Msg )

This is a bit inneficient, since for each andThen I do a Cmd.batch that is just going to be batching Cmd.nones for all the functions that do not create side effects. But in practice, I find the runtime overhead of this to be negligable.

We do something like this, but more like @rupert said:

(model, Cmd.none)
    |> Return.andThen doOneThing
    |> Return.andThen doTheOtherThing

Each andThen can return a new model and batch more commands
https://package.elm-lang.org/packages/Fresheyeball/elm-return/latest/Return#andThen

This pattern has been great for us.

2 Likes

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