
In my previous post I talked about using Lens
es 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 Foo
s (and similarly with Bar
s), 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 Int
s 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.unpack
s 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??).

…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 String
s, 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 Lens
es 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: