Do you want to encapsulate a bit of state, but find updating components daunting & the boilerplate cringe-worthy? Well, you probably don’t want or need this since you likely just want to keep all or as much of the state at the top-level, but in case you are building a true, standalone component, then this might be of use.
After watching much of the Elm Conf talks—particularly the Q&A panel—there was a decent amount of discussion about components. Most responses were along the lines of “try not to use them” & “use them where appropriate—where you need some local state”. This is very true, but what happens when you do need components? The first thing you’ll notice is that it’s cumbersome & the general reaction from the community is that by making it cumbersome you will be influenced to not use components as often. …But you will probably have to do it eventually & so it still should to be addressed.
…Think about functions as the key part of reuse … when you want to break out some logic, always reach for a function. So if your update [function] is getting too crazy, make a helper function.
Q&A Panel: https://www.youtube.com/watch?v=LCNs92YQjhw&t=23m37s
So let’s talk about what this update helper function would look like!
A basic understanding Isaac Shapira’s Return
monad (or Haskell’s Writer
monad) will be required. I highly suggest reading the author’s blog post for context, “The Return Monad: Harness the power of ( model, Cmd msg )”.
By no means is this the right way to do Elm as that is subjective. However, this is how I’ve started writing my update
function in a nearly 8000-line production-ready code base.
Return.Optics
, package here, is a utility library extending Return
with Monocle
making a clean, concise API for doing Elm component updates in the context of other updates. Initially it includes helper functions around refraction—the bending of light. Like viewing a straw being inserted into a glass of water, we’ll use a Lens
to bend our top-level update function into our component update, & when we pull it out, well be left with an unbent ( model, Cmd msg )
of the Elm architecture, slicing down the size & simplifying our code.
If that doesn’t make sense, you’re in luck because we’re about to go over an example.
Suppose we have this trivial, toy component & model…
Models
module Model exposing (Model)
import Checkbox.Model as Checkbox
type alias Model =
{ pageTitle : String
, checkbox : Checkbox.Model
}
module Checkbox.Model exposing (Model)
type alias Model =
{ checked : Bool
}
Msgs
module Msg exposing (Msg(..))
import Checkbox.Msg as Checkbox
type Msg
= TitleChange String
| CheckboxMsg Checkbox.Msg
module Checkbox.Msg exposing (Msg(..))
type Msg
= CheckMe Bool
Assuming we have built up some cmdWeAlwaysDo
, with the standard library we’d write updates like this:
Stardard Updates
module Update exposing (update)
import Checkbox.Update as Checkbox
import Model
import Msg exposing (Msg(..))
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
let
cmdWeAlwaysDo : Cmd Msg
cmdWeAlwaysDo =
-- insert a real command in a non-toy app
Cmd.none
in
case msg of
TitleChange title ->
( { model | pageTitle = title }, cmdWeAlwaysDo )
CheckboxMsg cbMsg ->
let
( cbModel, cbCmd ) =
Checkbox.Update cbMsg model.checkbox
in
{ model | checkbox = cbModel }
! [ Cmd.map CheckboxMsg cbCmd
, cmdWeAlwaysDo
]
module Checkbox.Update exposing (update)
import Checkbox.Model as Model
import Checkbox.Msg as Msg exposing (Msg(CheckMe))
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
CheckMe bool ->
( { model | checked = bool }, Cmd.none )
We can start to clean this up with Return
Return Update
module Update exposing (update)
import Return exposing (Return)
import Checkbox.Update as Checkbox
import Model
import Msg exposing (Msg(..))
update : Msg -> Model -> Return Msg Cmd
update msg =
let
cmdWeAlwaysDo : Cmd Msg
cmdWeAlwaysDo =
-- insert a real command in a non-toy app
Cmd.none
in
Return.singleton
>> Return.command cmdWeAlwaysDo
>> case msg of
TitleChange title ->
Return.map (\m -> { m | pageTitle = title })
CheckboxMsg cbMsg ->
(\(model, cmd) ->
let
( cbModel, cbCmd ) =
checkboxUpdate cbMsg model.checkbox
in
{ model | checkbox = cbModel }
! [ Cmd.map CheckboxMsg cbCmd
, cmd
]
)
module Checkbox.Update exposing (update)
import Return exposing (Return)
import Checkbox.Model as Model
import Checkbox.Msg as Msg exposing (Msg(..))
update : Checkbox -> CheckboxModel -> Return Msg Model
update msg =
Return.singleton
>> case msg of
CheckMe bool ->
Return.map (\m -> { m | checked = bool })
It’s a little hard to see in a toy example, but when you start building a large update function the Return
API & its monadic properties give you a way to update little parts of your model/cmd tuple without dropping commands (because the Platform.Cmd
s are monoidal with Cmd.batch
& Cmd.none
)… If you look at the TitleChange
, it’s super clean, & we forget about commands since we’re only updating a part of the model & we get to cut to the meat of our intentions. Also because we’re piping in the singleton return, we can use applicative style programming.
However, there’s a problem when we look at the toy component’s update. Things actually managed to get uglier because we need to run the component’s update to get our sub model/cmd tuple, update the model, & then make sure we don’t drop that cmdWeAlwaysDo
along with mapping the command to the top-level message. So can we clean that up?
Lenses
Using the defined [.abbr]#API#s of Monocle
’s Lens
& Optional
, we can get to something (subjectively) cleaner.
But doesn’t the Elm community consider lenses an anti-pattern? Well, sort of. In the case where I was talking in Slack about lenses I got a lot of flack with comments like needing lenses is a code smell, but let’s take a second to talk about lenses. In the case of Lens
we have a type signature that looks like this:
type alias Lens a b =
{ get : a -> b
, set : b -> a -> a
}
Lenses don’t have to be scary. What we have here is a defined API for getter & setter function—nothing more. So rather than creating a function to set a complicated field in your record floating in the abyss of your codebase, lenses give you a defined way to write & bundle up these functions—backed by math & laws—and they compose which provides you a lot of power. The biggest downside is that without derivation & macros you have to write your own lenses. (But it’s not hard!)
But I digress; fears aside, let’s get back to the update function by creating some lenses for our model using Monocle.Lens.Lens
as a constructor:
Lensed Models
module Model exposing (..)
import Monocle.Lens exposing (Lens)
import Checkbox.Model as Checkbox
type alias Model =
{ pageTitle : String
, checkbox : Checkbox.Model
}
pageTitlel : Lens Model String
pageTitlel =
Lens .pageTitle (\p m -> { m | pageTitle = p })
checkboxl : Lens Model Checkbox.Model
checkboxl =
Lens .checkbox (\c m -> { m | checkbox = c })
module Checkbox.Model exposing (..)
import Monocle.Lens exposing (Lens)
type alias Model =
{ checked : Bool
}
checkedl : Lens Model Bool
checkedl =
Lens .checked (\c m -> { m | checked = c })
This doesn’t look so bad. Lens
as a constructor means we first pass in our et
function, & then follow it up with our set
function. What’s neat is as the app grows, we’ll have these getters & setters in place to reuse.
Let’s take a peek at Return.Optics.refractl
’s source:
refractl : Lens pmod cmod -> (cmsg -> pmsg) -> (cmod -> Return cmsg cmod) -> ReturnF pmsg pmod
refractl lens mergeBack fx ( model, cmd ) =
lens.get model
|> fx
|> Return.mapBoth mergeBack (flip lens.set model)
|> Return.command cmd
- Breaking down this type signature in relation to our toy
-
-
A
Lens
of top-level model & the component model (Model
&Checkbox.Model.Model
). -
A function to get our component’s
Cmd
to our top-levelCmd
(Checkbox.Msg.Msg → Msg
). -
A component update function (
Checkbox.Update.update
). - A top-level return (aka model+cmd tuple).
-
Returning a top-level return. (Note:
Return.ReturnF
is an endomorphism which just allows us to combine 4 & 5 inputing & returning the same type).
-
A
- And breaking down the function itself
-
-
Using our Lens,
let
our component from the model. -
Run our component’s
update
on it. -
Map on our component
update
’sCmd
with that function that that gets to our top-levelMsg
along with using theLens
to set component with theupdate
’s changes to the component’s model. - Make sure we append in all other commands we’ve built up.
-
Using our Lens,
This might seem a bit heavy, so let’s see it in practice with the CheckboxMsg
(along with using our other lenses):
Refract Update
module Update exposing (update) import Return exposing (Return) import Return.Optics exposing (refractl) import Checkbox.Update as Checkbox import Model import Msg exposing (Msg(..)) update : Msg -> Model -> Return Msg Cmd update msg = let cmdWeAlwaysDo : Cmd Msg cmdWeAlwaysDo = -- insert a real command in a non-toy app Cmd.none in Return.singleton >> Return.command cmdWeAlwaysDo >> case msg of TitleChange title -> Return.map (.set Model.pageTitlel title) CheckboxMsg cbMsg -> refractl Model.checkboxl CheckboxMsg (Checkbox.update cbMsg)
module Checkbox.Update exposing (update)
import Checkbox.Model as Model
import Checkbox.Msg as Msg exposing (Msg(..))
update : Msg -> Model -> Return Msg Model
update msg =
Return.singleton
>> case msg of
CheckMe bool ->
Return.map (.set Model.checkedl bool)
I don’t know about you, but that really shrinks & cleans up updating that component. We went from an update function of 20 lines initially with the standard library, to 24 in the base Return
, to 16 with refractl
. That starts to add up fast in a large application & less lines of code means less lines of code to maintain. With this style, we can cut out component update into our main update & transform both models.
If you really don’t want to have to build the Lens`es, using substitution you can use the constructor to build them inline (e.g. `refractl (Lens .checkbox <| \\c m → { m | checkbox = c }) CheckBoxMsg <| Checkbox.update cbMsg
), but that won’t be reusable.
Partnered with refractl
is refracto
that takes an Optional
instead of a Lens
.
Here’s the diff
of the main update:
--- a.elm 2016-10-20 20:08:31.785515404 -0600
+++ b.elm 2016-10-20 20:09:01.896645780 -0600
@@ -1,11 +1,13 @@
module Update exposing (update)
+import Return exposing (Return)
+import Return.Optics exposing (refractl)
import Checkbox.Update as Checkbox
import Model
import Msg exposing (Msg(..))
-update : Msg -> Model -> ( Model, Cmd Msg )
-update msg model =
+update : Msg -> Model -> Return Msg Cmd
+update msg =
let
cmdWeAlwaysDo : Cmd Msg
@@ -13,16 +15,12 @@
-- insert a real command in a non-toy app
Cmd.none
in
- case msg of
- TitleChange title ->
- ( { model | pageTitle = title }, cmdWeAlwaysDo )
+ Return.singleton
+ >> Return.command cmdWeAlwaysDo
+ >> case msg of
+ TitleChange title ->
+ Return.map <| .set Model.pageTitlel title
- CheckboxMsg cbMsg ->
- let
- ( cbModel, cbCmd )
- Checkbox.Update cbMsg model.checkbox
- in
- { model | checkbox = cbModel }
- ! [ Cmd.map CheckboxMsg cbCmd
- , cmdWeAlwaysDo
- ]
+ CheckboxMsg cbMsg ->
+ refractl Model.checkboxl CheckboxMsg <|
+ Checkbox.update cbMsg
Takeaway
Using the defined API for getters & setters of lenses, we can pass in a reusable, composable lens in as an argument. We know a lens is the appropriate data model for the argument because we require both the getter & the setter to merge these new values into our model. The way we partially apply the message into a component’s update
, we can reuse the current standard for writing components with its own update meaning we can follow & borrow existing documentation & components using this interface. We can also easily add more & more components without dirtying our update function. Sometimes you need to hijack a component’s message at the top-level update to modify something else on the state & with these lenses already built, we’d have the ability to update nested components with much less boilerplate.