Given
type Msg = A | B | C
Say you are in the A
branch of your update function, and want to execute the code of the C
branch. You go to the Core.Task
documentation, find Task.perform
and - with a little type tetris - write.
Task.perform identity identity (Task.succeed C)
I think this code is problematic and that there is a better way.
What is wrong
First we need a bit more code
update : Msg -> Model -> (Cmd Msg, Model)
update msg model =
case msg of
A ->
Debug.log "in A" ( model, Task.perform identity identity (Task.succeed C) )
B ->
Debug.log "in B" ( model, Cmd.none )
C ->
Debug.log "in C" ( model, Cmd.none )
My problem with this code is that using Task.perform
introduces asynchronicity into the update process. The C
branch is only executed when Elm’s scheduler gets to it: in theory that could be many seconds and model updates later. The model
that the A
branch produces might not be the model
that the C
branch gets.
Asynchronous code is hard to reason about, so I’d like to prevent introducing it when possible . When using Task.perform
in combination with Task.succeed
, there is almost definitely a better way.
Note that Task.perform
is the only sensible thing to do when all you have is some task (for instance http requests, setting window size, getting page visibility). My problem is strictly with using Task.perform
in combinations with Task.succeed
.
Alternatives
Really there are two options.
Recursively calling update
I don’t really like this, because replacing asynchronicity with recursion is only a slight improvement.
EDIT: After some discussion with @cobalamin and @opsb, I think the above needs a better motivation.
I think function extraction is clearer than calling update and basically saying “execute what SomeMsg would execute”, even though the end result is the same. Extracting functionality into functions makes the code more composable and reusable (copy-paste into a different project, use from a different module).
update : Msg -> Model -> (Cmd Msg, Model)
update msg model =
case msg of
A ->
Debug.log "in A" (update C model)
B ->
Debug.log "in B" ( model, Cmd.none )
C ->
Debug.log "in C" ( model, Cmd.none )
Refactor the branch into a function
Just using a function is by far the simplest option. A downside is that some of the update logic now lives outside of the update
function, but when the extracted function has a descriptive name, that shouldn’t be a problem.
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
A ->
Debug.log "in A" ( theCBranch model )
B ->
Debug.log "in B" ( model, Cmd.none )
C ->
theCBranch model
theCBranch : Model -> ( Model, Cmd Msg )
theCBranch model =
Debug.log "in C" ( model, Cmd.none )
P.S.
In the case that both the A
and the C
branch produce commands, here is a helper for composing these actions
andThen : ( Model, Cmd msg ) -> ( Model -> ( Model, Cmd msg ) ) -> ( Model, Cmd msg )
andThen ( beginModel, cmd1 ) advance =
let
( newModel, cmd2 ) = advance beginModel
in
( newModel, Cmd.batch [ cmd1, cmd2 ] )
The A
branch with commands ( substitute whatever command for Cmd.none
) would become
-- recursive
A ->
( model, Cmd.none ) `andThen` update C
-- extracted function
A ->
( model, Cmd.none ) `andThen` theCBranch