xilnocas/step/4.1.1

NOTE: the latest version is 4.1.4

Renamed to nicklawls/step. That is where the repository lives now. It has not been published as a package under the new name.

Versions published as xilnocas/step still install under that name. 11 versions of 15 come from the new location (see their notes).

Rerouted (renamed, verified). The GitHub user or repository was renamed. The tag is still there, at the new name. The Lamdera package server serves a copy recovered from https://github.com/nicklawls/step.

What we checked: The repo moved to github.com/nicklawls/step. Rebuilt with git archive from commit d87eabb7ae74 of https://github.com/nicklawls/step (zip prefix xilnocas-step-d87eabb/, America/Los_Angeles times): the zip is byte-identical to the one package.elm-lang.org recorded, SHA-1 63b332755c21b3fa5cfcd25dc25f4b256d4cf917.

The Lamdera compiler uses this copy and prints a one-line notice saying so. elm itself fails to download this version.

Details · package.elm-lang.org's zip: https://github.com/xilnocas/step/zipball/4.1.1/

step

An experimental package for clean update functions

Overview

Use this package to write update functions in your Elm app.

Instead of returning a (Model, Cmd Msg) from your update function, you'll return a Step Model Msg a, with or without the a variable filled in. I know, I know, there are three type variables there. In return for having to look at three freakin type variables all day long, code that looks like this:

let
    (newLogin, loginCmd) =
        Login.update loginMsg model.login
in
    ({ model | login = newLogin }, Cmd.map LoginMsg loginCmd)

will come out looking like this

Login.update loginMsg model.login
    |> Step.within (\w -> { model | login = w }) LoginMsg

And that's just the beginning.

The goal is that by using step, you'll be able to

  1. Express common update function patterns easily and safely
  2. Think and talk about these patterns in a way that makes sense with TEA
  3. Notice more easily how many states your app has, helping you "make impossible states impossible"

Usage

Here's a quick tour of some important features

Returning state, maybe with commands

A lot of the time, all your update function does is update the state of the app. You usually return something like this

(newModel, Cmd.none)

With step, you'll return this

Step.to newModel

If you want to fire off a command in addition to the state change, you'd do so like this.

( newModel
, Http.send ServerResponse (Http.get "/fruits" fruitDecoder)
)

With step, it looks like this

Step.to newModel
    |> Step.command (Http.send ServerResponse (Http.get "fruits.json" fruitDecoder))

Calling nested update functions

Code that looks like this

update : Msg -> Model -> (Model , Cmd Msg)
update msg model =
    case msg of
        LoginMsg loginMsg ->
            let
                (newLogin, loginCmd) =
                    Login.update loginMsg model.login

            in
                ({ model | login = newLogin }, Cmd.map LoginMsg loginCmd)                    

turns into this

import Step exposing (Step)

update : Msg -> Model -> Step Model Msg a
update msg model =
    case msg of
        LoginMsg loginMsg ->
            Login.update loginMsg model.login
                |> Step.within (\w -> { model | login = w }) LoginMsg

Handling invalid transitions

Step.stay is sort of like Nothing, but specialized for Steps. It lets you say "on this combination of Model and Msg, I don't want the state to transition." Basically, any time you want to say

(model, Cmd.none)

just say

Step.stay

But this isn't just terser syntax. Step.orElse lets you combine a bunch of isolated steps, and return only the first one that isn't a stay

let
  stepCalendar =
      case msg of
          AddEvent e ->
              Step.to { model | calendar = e :: model.calendar }

          _ ->
              Step.stay

  stepContacts =
      case msg of
          AddContact c ->
              Step.to { model | contacts = c :: model.contacts }

          _ ->
              Step.stay

in
  stepCalendar
      |> Step.orElse stepContacts
      -- if msg is AddContact, will return value of stepContacts

This can be super useful as you're iterating on an app, where you know roughly which pieces of state are isolated from one another, but you don't want to split messages up into their own types quite yet.

Returning extra data from update

Step.exit and Step.onExit work together to implement a version of what's sometimes called the "OutMsg" pattern, which is really just returning context other than the (Model, Cmd Msg) from an update function.

A special case of that pattern that I've found really useful is when the context only comes out when update function makes its final transition.

As an example, think of a login interaction: It starts, proceeds as the user types their info, then some REST call is made, then when all is said and done we're left with a User. We can encode that idea really easily

module Login exposing (..)


update : Msg -> Model -> Step Model Msg User
update msg model =
    case msg of

      -- Usual filling out of a login form ...

       LoginSucceeded user ->
          Step.exit user

        ...

Then, whoever uses our Login module can use Step.onExit to incorporate this data into their own Step

module Main exposing (..)

import Login


update : Msg -> Model -> Step Model Msg a
update msg model =
    case (msg, model) of
        (LoginMsg loginMsg, LoggingIn loginModel) ->
            Login.update loginMsg loginModel
                |> Step.within LoggingIn LoginMsg
                |> Step.onExit (\user -> Step.to (LoggedIn user))
         ...

My hunch is that this method of building update functions that return data at the end of an interaction covers most of the use cases of the "OutMsg" pattern, and ultimately leads to simpler, easier to understand code. I'm excited so see if the community finds this to be true as well!

Example app

To provide an orienting example, I've forked Richard's elm-spa-example, and converted all the update functions to return Steps. It so happens that that app is architected in a way that makes step less useful; there were no opportunities to use orElse or onExit. Still, looking at the diff will give you an idea of how things translate.

Prior Work

The idea for this kind of package is not new. Step wouldn't be a thing without inspiration from the following libraries

Fresheyeball/elm-return

  • My original inspiration for this sort of package. The pattern of appending commands with a pipeline-friendly function really appealed to me
  • Over-relies on Haskelley lingo
  • Too much API. As an example, Step doesn't have an andThen function exposed, because I've never had a use for one.

Janiczek/cmd-extra

  • Pleasingly simple
  • Too little API ( :) )

Chadtech/return

  • A Return3 is the closest thing to a Step I've seen. The incorp function is very similar in purpose to the onExit function, and seeing it helped me realize I was on the right track.

  • Return3 lets you return state, commands, and extra stuff all at the same time, which I suspect is unnecessary. The idea of an interaction's behavior being constantly dependent on data from a sub-interaction makes me think that the two should probably be folded into one. In contrast to Return3, Step optimizes for when there's a clean break between the two.

  • I struggle to form a mental model for what a Return3 is. It's a name for the code pattern, whereas Step tries to be a name for a higher-level entity: a step in TEA's update loop.

Installation

elm install xilnocas/step