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 39824a22d6a6 of https://github.com/nicklawls/step (zip prefix xilnocas-step-39824a2/, America/Los_Angeles times): the zip is byte-identical to the one package.elm-lang.org recorded, SHA-1 8a705575dfea37084db1e35e20b8fae87d5e05a6.
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.4/
An experimental package for clean update functions
Use this package to write update functions in your Elm app.
Code that used to look 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 }) LoginMsgInstead 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, you'll get the above cleanliness improvement and a bunch of other goodies.
The goal is that by using step, you'll be able to
Here's a quick tour of some important features
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 newModelBefore, if you wanted to fire off a command in addition to updating state, 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))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 }) LoginMsgThe idea behind the name within is that you're transforming a step in some smaller interaction to a step within some larger interaction.
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.stayAnd this isn't just a 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 stepContactsThis can be handy 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.
updateOften when splitting an app into a set of distinct update functions, some of those update functions are involved in producing a value other than the state that the calling update function needs to consume.
As an example, think of a login interaction: It starts, proceeds as the user types their info, then some REST call is made on submit, then when all is said and done we're left with a User. We can encode that idea really easily with Step.exit
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 userHere we see the third type variable come into play. It represents the type of data that eventually gets returned in the final Step of the interaction we're modeling.
Now, whoever uses our Login module can use Step.onExit to incorporate this data into their own Step with the onExit function.
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))Elm veterans might notice that this bears some resemblance to the "OutMsg" pattern. I think calling it that is more confusing than helpful. It's not a "Msg" (in the elm sense) that's coming back per se, it's just a normal elm value that you can use how you please.
And, crucially, we're restricting ourselves to interactions in which returning a value of type a is the last thing that happens, in some sense. This ensures we're not creating some tightly coupled conversation between two pieces of state, a-la OOP.
My experience is that building modules around these sorts of exit boundaries ultimately leads to easier to understand code. I'm excited to see if the community finds this to be true as well!
You can use step at any point in an app. But at some point, you're going to have to convert Steps back into the (model, Cmd msg) that The Elm Architecture demands. The easiest way to do this is with Step.asUpdateFunction. Just pass it an update function defined with Step, and it'll spit out a TEA-compatible update function that does what you'd expect.
We also provide Step.run if you want more control over happens when the Step is a stay.
There is some subtle type trickery going on in these functions with the Never type. All you should have to know is that in order to pass something to run or asUpdateFunction, the third type variable in the Step (named a in the docs) can't be filled in with a concrete type. If it is, you need to call onExit on it and consume the return value in some way to get the types to line up.
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.
Under Construction
The idea for this kind of package is not new. Step wouldn't be a thing without inspiration from the following libraries
Step doesn't have an andThen function exposed, because I've never had a use for one.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, 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.
elm install xilnocas/step