Crocodiles & Alligators

Practical Lenses for Fun & Profit in Elm

Crocodiles & Alligators

In my previous post I talked about using Lenses as a way to clean up some of the destructuring boilerplate in Elm’s update function, but we can use these to do some other nifty tricks.

Recently at work I had a problem involving some nested data types in an Either. So one thing you’ll notice about Elm is that you have to push in very concrete types thru Elm’s port system—and you definitely can’t have polymorphism. There’s nothing wrong with that, & it can give you some nice guarantees. So here’s the problem I ran into: I have two record types that are similar, but different enough that I needed to have different types; let’s assume these as type Foo & Bar that correspond to a field on our model. At the end of the port sending in List Foos (and similarly with Bars), I marshaled List Foo with:

import Monocle.Lens exposing (Lens)

type alias Model =
    { foobar : List (Either Foo Bar)
    }

foobar : Lens Model (Either Foo Bar)
foobar =
    Lens .foobar (\fb m -> { model | foobar = fb })
import Either exposing (Either(Left, Right))
import Return exposing (ReturnF)

update : Route -> Msg -> ReturnF Msg Model
update route msg =
    case msg of
        -- …
        FoosReceived foos ->
            List.map (marshalFoo >> Left) foos
                |> .set Model.foobar
                |> Return.map
        BarsReceived bars ->
            List.map (marshalBar >> Right) bars
                |> .set Model.foobar
                |> Return.map
        -- …

This makes a lot of sense since I can define what’s going to be shoved into the port for both types, do the modifications I need—like turning Ints to their representative ADTs—and then tagging it Left or Right to distiguish them. So where this starts to get funky tho is at the view layer where now you’re going to be pulling apart this structure a lot which is going to involve a lot of repetitious Either.unpacks that get ugly. This is what we’re going to solve with our buddy the Lens.


Toy Time with the Order Crocodilia

So let’s talk about alligators & crocodiles (that’s why you’re here, right?). Unless you’re a biologist, nerd, or a pedant (like me), these two aquatic reptiles are the same thing. Alas, they are not. For instance: alligators are normally grey, shorter, U-snouted, freshwatery, & are only fans of living in the US & China—my educated guess is because they are also fans of the military-industrial complex & imperialism (mandible-fest destiny??).

…But you know what they really don’t have in common? The data structure for their names in my toy example.

import Either exposing (Either)

type alias Crocodile =
    { firstName : String
    , lastName : String
    }

type alias Alligator =
    { fullName : String
    }

type alias Model =
    { poppycroc = List (Either Crocodile Alligator)
    }

These guys always gotta be similar but just a little bit different. For the sake of simplicity for this toy concept, a full name looks like “Bob Saget”—no mononyms—where “Bob” is the first name & “Saget” is the last name.

Warning

Do not use first & last name in production software as it is biased towards cultures that use this structure. Given & family name are improvements as it covers for instance Asian cultures where the family name come first or Spanish naming customs with double last names, but preferred & legal name are often even better. Why? Because the using the names in a split fashion is in many cases not relevant & in a lot of contexts you want users to be able to specify a preferred name to cover nicknames & exclude dead names where a single name input covers any naming variations (by not being specific).

So what happen at the view layer?

import Either exposing (Either)
import Function.Extra as Fn
import Html exposing (..)
import List.Extra as List

view : Model -> Html Msg
view { poppycroc } =
    div []
        [ h1 [] [ text "Our Reptile Names" ]
        , ul [] <|
            List.map
                (Either.unpack (Fn.map2 (++) .firstName .lastName)
                    .fullName
                    >> \n -> li [] [ text n ]
                )
                poppycroc
        ]

So what we have here is some point-free goodness in that Either.unpack that on the Left gets the firstName & lastName fields & lifts them across the (++), the append infix, to make a full name, & on the Right we just access the fullName. Those Either.unpack to Strings, & are then shoved into a text node, then into a list List.Extra.singleton, & finally into an Html.li. Neat, yes, but what what are we going to do if we need to use that full name all over the app as you normally do? Well, we could store that unpackery to a function or we could use a Lens.

module CMT exposing (..)

import Either exposing (Either)
import Function.Extra as Fn
import String

fullName : Lens (Either Crocodile Alligator) String
fullName =
    let
        setc : String -> Crocodile -> Crocodile
        setc n c =
            case String.split " " n of
                [ fn, ln ] ->
                    { c | firstName = fn, lastName = ln }

                -- I thought we said no mononyms… you’re killing this
                -- example. IRL, I’d have a validation step first for
                -- setting the name.
                _ -> c

        seta : String -> Alligator -> Alligator
        seta n a =
            { a | fullName = n }
    in
        Lens
            -- getter
            (Either.unpack
                (Fn.map2 (++) .firstName .lastName)
                .fullName
            )
            -- setter
            (\name -> Either.mapBoth (setc name) (seta name))

This is Lens that traverses the Either. It’s reusable & composable (we could even use Lenses with with subtypes as well!). So let’s look at that new view:

import Html exposing (..)
import List.Extra as List
import CMT

view : Model -> Html Msg
view { poppycroc } =
    let
        names : List (Html Msg)
        names =
            List.map (.get CMT.fullName >> \n -> li [] [ text n ]) poppycroc
    in
        div []
            [ h1 [] [ text "Our Reptile Names" ]
            , ul [] names
            ]

Takeaway

That Lens function is so short in the view, we can actually one-line this list item. We have a way to peek at our Either Crocodile Alligator thru a Lens that let’s us act as tho these two similar-but-different types were actually the same type.

At my job, I used this across an Either & called List.head on an inner list containing records that all had a field of the same value (like a primary key) which saved me a lot of steps & really cleaned up my code. It also afforded me the ability to update all the records in the list with the new field too!

Hopefully this excites you to think of other clever, useful ways to use to access more complex data structures.