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
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?
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”.
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.
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.
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, …
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)
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.