Crocodile & Alligator

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 through the port system—and you definitely can’t have polymorphism. There’s nothing wrong with that, and 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 and 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 though 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 and crocodiles (that’s why you’re here, right?). Unless you’re a biologist, nerd, or pedantic asshole (like me), these two aquatic reptiles are essentially the same thing. Alas, they are not, for instance: alligators are normally grey, shorter, U-snouted, freshwatery, and only fans of living in the US and China—my educated guess is because they are also fans of the military-industrial complex and imperialism (mandible-fest destiny??).

Martial alligator

…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 and “Saget” is the last name.

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)
                    .fullNa|e
                    >> \n -> li [] [ text n ]
                )
                poppycroc
        ]

So what we have here is some point-free goodness in that unpack that on the Left gets the firstName and lastName fields and lifts them across the (++), the append infix, to make a full name, and on the Right we just access the fullName. Those unpack to Strings, and are then shoved into a text node, then into a list singleton, and finally into an 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


type alias ChevroletMovieTheater =
    Either Crocodile Alligator


fullName : Lens ChevroletMovieTheater 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 and 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 through a Lens that let’s us act as though these two similar-but-different types were actually the same type.

At my job, I used this across an Either and called 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 and 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. If you need some inspiration: