Chadtech/return

Helpers for update functions


Keywords
elm, tuples
Install
elm-package install Chadtech/return 1.0.1

Documentation

Return

This package is for handling the subtle architectural problems associated with nested update functions in big Elm applications. There are no simple principles at work here; this package is just a product of my cumulated experience from writing a lot Elm and paying attention to what other people do when they write Elm. Immediately below is a code example, and further below is an explanation of how I got here.

Example

-- Main.elm --
import Login
import Return2 as R2
import Return3 as R3


type alias Model =
    { page : Page 
    , user : Maybe User
    }


type Page
    = Login Login.Model
    | Home


type Msg
    = LoginMsg Login.Msg


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        LoginMsg subMsg ->
            case model.page of
                Login subModel ->
                    subModel
                        |> Login.update subMsg 
                        |> R3.mapCmd LoginMsg
                        |> R3.incorp handleLoginReply model

                _ ->
                    model
                        |> R2.withNoCmd


handleLoginReply : Login.Model -> Maybe Login.Reply -> Model -> (Model, Cmd Msg)
handleLoginReply subModel maybeReply model =
    case maybeReply of
        Nothing ->
            model
                |> setPage Login subModel
                |> R2.withNoCmd

        Just (Login.UserLoggedIn user) ->
            { model
                | page = Home
                , user = Just user
            }
                |> R2.withNoCmd


setPage : (subModel -> Page) -> subModel -> Model -> Model
setPage pageCtor subModel model =
    { model | page = pageCtor subModel }


-- Login.Elm --

import Return3 as R3 exposing (Return)
import Return2 as R2


type alias Model =
    { field : String }


type Msg
    = FieldUpdated String
    | SubmitClicked
    | LoginSucceeded User


type Reply
    = UserLoggedIn User


update : Msg -> Model -> Return Model Msg Reply
update msg model =
    case msg of
        FieldUpdated str ->
            { model | field = str }
                |> R3.withNothing

        SubmitClicked ->
            model
                |> R2.withCmd (submitRequest model)
                |> R3.withNoReply

        LoginSucceeded user ->
            model
                |> R2.withNoCmd
                |> R3.withReply (UserLoggedIn user)

Explanation

In Elm, our update functions return a tuple containing our Model and whatever side effects we want to occur outside the Elm run-time.

( Model, Cmd Msg )

In practice, that means we have to type stuff like this.

    ( editThing model, thingCmd (getData model))

Unfortunately this turns out to be not so nice. We have to type this tuple a lot, and doing so is a hassle because there are characters on the left, right, and center of the tuple you have to type. It would be much easier if you only had to put your cursor in one spot rather than jump around. As an alternative some of us began using our own infix operators; most notably the folks at NoRedInk use the "rocket" operator.

    model => thingCmd

(=>) : a -> b -> (a, b)
(=>) model cmd =
    (model, cmd)

Thats pretty good. But as I write this, the expectation is that the upcoming Elm 0.19 wont allow custom infix operators (which is probably for the best). We will need a new solution. I really like Janiczek/cmd-extra. His package exposes some extremely readable functions, like withCmd.

    model 
        |> withCmd cmd

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

Its a little bit more verbose, but it reads really nice and it plays well with whatever else is going on in your update function. If this were the end of the story, I would just settle on Janiczek's package, but its not. Big Elm applications usually have sub-update functions, sub-models, and sub-msgs. The communication between parent and child update functions can be kind of complicated, and often we need to transform values after they have been bundled into tuples. Our update function technique has to work well in the broader context of what update functions are doing in our application.

Richard Feldman uses the ExternalMsg type for communication from child update functions to parent update functions. His update functions result in something that looks like this.

    ((Model, Cmd Msg), ExternalMsg)

The ExternalMsg carries information from the sub module to its parent module. The best example of this is having a login page with its own update function, that needs to tell the main update function who the user is once they successfully log in. The sub update function in your login page doesnt just return a sub-model and sub-cmds, it also returns information that the application as a whole needs to know, like that the user session has changed.

That works very well. One criticism I have however, is nested tuples are kind of hard to work with. The model is in a tuple in a tuple. So if you want to access it- say to transform it- you need to do Tuple.first >> Tuple.first. Flat data structures are nicer, so I have learned to do triples instead.

    (Model, Cmd Msg, ExternalMsg)

Also, I found that the name ExternalMsg didnt make much sense. Regular Msgs reflect external events that actually happened in your application that have indeterminate consequence. Stuff like mouse clicks, or http responses; your application just knows what happened and thereafter needs to figure out what to do. ExternalMsgs arent the same kind of thing despite what the name implies. They represent interal results from within your application which usually have explicit consequence. I instead started calling them Reply since they are like replies to the news sub-modulereceive from the parent-modules. Also I made it into a Maybe Reply, so you can consider the possibility of no reply abstractly, without assuming any particular Reply type.

    (Model, Cmd Msg, Maybe Reply)

Improvements from this point are harder and more tenuous, but I have also learned a bit from Fresheyeball/elm-return as well. Sub-models need to be incorporated back into their parent-models, and usually in very regular and predictable ways, such as just being a field inside a record. Fresheyeball's package exposes functions that simplify that incorporation process. Unfortunately, I think Fresheyeball's package indulges a lot of functional programming stuff beyond its usefulness (and it uses infix operators, so its usefulness wont last into 0.19). But regardless, his approach to formalizing and mutating return results is a good one that I have tried to reproduce in this package.