Optical Swordplay With Components

Using Lenses & Monads to Refract the Elm Update

Abstract

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.

—Evan Czaplicki, 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 writer’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 since we’re about to go over an example.

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

         handleMsg : Msg -> Return Msg Cmd
         handleMsg =
    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 (since the Platform.Cmds are monoidal with 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 APIs 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 the proprietary Slack channel 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—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 set 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

  1. A Lens of top-level model & the component model (Model & Checkbox.Model.Model).
  2. A function to get our component’s Cmd to our top-level Cmd (Checkbox.Msg.Msg -> Msg).
  3. A component update function (Checkbox.Update.update).
  4. A top-level return (aka model+cmd tuple).
  5. Returning a top-level return.
Note

Return.ReturnF is an endomorphism which just allows us to combine 4 & 5 inputing & returning the same type.

And breaking down the function itself

  1. Using our Lens, let our component from the model.
  2. Run our component’s update on it.
  3. Map on our component update’s Cmd with that function that that gets to our top-level Msg along with using the Lens to set component with the update’s changes to the component’s model.
  4. Make sure we append in all other commands we’ve built up.

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 (:ab:`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 Monocle 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.