This blog post is inspired by a Stack Overflow question. And it turns out I made a package for this; You can see the full package here (Elm does not allow publishing packages on platforms other than Microsoft™ GitHub which hamstrings the freedom of all of us).
So in Elm all <input>
s, <select>
s, & <textarea>
s get their value
out as a String
—we can see that that is the case with Html.Event.targetValue
. People will initially get a bit caught when they want to parse an integer from that String
.
Do I have my
Msg
take aString
or doJson.Decode
the value and then put theResult String Int
on the message? Or do IResult.withDefault
and just stick a0
in theMsg
??
Where this gets a little more confusing and more prone to error is the big, bad <select>
. A <select>
is basically the UI representation of a union type or ADT. As such we want to display it. The problem is: strings. An Html.option
can have an attribute Html.Attributes.value
which is a String
. Then when an option is selected, we want to turn that String
back into our ADT.
Enter the Prism.
type alias Prism a b =
{ getOption : a -> Maybe b
, reverseGet : b -> a
}
If we have a quick look, the Prism
is a data structure for data transformation that just aren’t quite isomorphic. Some data can be isomorphic like a String.reverse
where we can get can go from "paddywhack"
to "kcahwyddap"
and back again without loss. With an ADT we can easily case out to a String
—and the compiler will yell if we don’t, but going back we’re constrained by the fact that the set of all string is infinite. Let’s look at an example:
type Color
= Red
| Blue
| Green
colorFromString : String -> Maybe Color
colorFromString s = case s of
"red" -> Just Red
"green" -> Just Green
"blue" -> Just Blue
_ -> Nothing
colorToString : Color -> String
colorToString c = case c of
Red -> "red"
Green -> "green"
Blue -> "blue"
We can see plain as day that in the case of colorFromString
we are returning a Maybe
where the string case falls out. This sure does look a lot like a Prism
. getOption
is Color -> String
and reverseGet
is String -> Maybe Color
. So let’s make that into Prism
then!
{-| You the developer are responsible for this `Prism`s correctness
-}
color_p : Prism String Color
color_p =
let
colorFromString : String -> Maybe Color
colorFromString s = case s of
"red" -> Just Red
"green" -> Just Green
"blue" -> Just Blue
_ -> Nothing
colorToString : Color -> String
colorToString c = case c of
Red -> "red"
Green -> "green"
Blue -> "blue"
in
-- Using `Prism` as a constructor
Prism colorFromString colorToString
Pretty neat, huh? How do we use it?
colorp.reverseGet Red
--=> "red"
colorp.reverseGet Green
--=> "green"
colorp.getOption "red"
--=> Just Red
colorp.getOption "mauve"
--=> Nothing
Hopefully some lightbulbs are going off in your head with this Color -> String -> Color
signature-ish thingy. The Prism
itself is written generically. Can we stay just as generic for a <select>
? You bet we can!
Making a Generic Select
selectp : Prism String a -> (Result String a -> msg) -> a -> List (Attribute msg) -> List ( String, a ) -> Html msg
selectp prism msger selected_ attrs labelValues =
let
resultFromString : String -> Result String a
resultFromString x =
case prism.getOption x of
Just y ->
Ok y
_ ->
Err ("Failed to get a valid option from " ++ toString x)
change : Decoder msg
change =
Decode.map (resultFromString >> msger) targetValue
opt : ( String, a ) -> Html msg
opt ( labl, val ) =
option
[ selected (selected_ == val)
, value (prism.reverseGet val)
]
[ text labl ]
in
select (on "change" change :: attrs)
(List.map opt labelValues)
Prism String a -> (Result String a -> msg) -> a -> List (Attribute msg) -> List ( String, a ) -> Html msg
is a pretty intimidating type signature, but it gives us all the levels of abstraction we need. Breaking down the signature one at a time…
-
Prism from a
String
to our thing,a
-
A function from the attempt to get—
Result String a
, wherea
is our thing—to a msg for the onChange - The selected value
-
List of
Html.Attributes
for the<select>
so you can have custom classes, etc. -
List tuples of
( String, a )
where theString
is the label for the option and thea
is our thing. -
…Returning some
Html
.
As far as the code, the onChange
is returning us a Result
like Http.send
so you’re gonna wanna use a message that can handle that Result
. Unfortunately, I can’t trust that you’ll implement the getOption
part correctly, so it makes absolute sense to return an Err
on failure. You can choose to drop it in your update
funciton with the message, or you can hold onto it to display an error.
How Does It Look In My View?
import Html exposing (..)
colorOptions : List ( String, Color )
colorOptions =
[ ( "❤️ Red", Red )
, ( "💙 Blue", Blue )
, ( "💚 Green", Green )
]
type alias Model =
{ selectedColor : Color }
type Msg
= ChangeColor (Result String Color)
view : Model -> Html Msg
view { selectedColor } =
-- Right Here ↓
selectp colorp ChangeColor selectedColor [] colorOptions
Demo Time!
Takeaway
The Elm meme right now is all about data modeling. Because of that, Prism
s hold a good place since ADTs are not comparable
(which is why dictionaries return Maybe
s). They can also help use in other sticky situtations like updating a model that contains an ADT. Since Prism
s are optical, they can compose as well. If your ADT contains a record like { foo = True }
, we can turn that Prism
into an Optional
and then compose with a Lens
. Check it:
import Monocle.Lens exposing (Lens)
import Monocle.Optional as Optional exposing (Optional)
import Monocle.Prism exposing (Prism)
type alias Foo =
{ foo : Bool }
type Bar
= Bar Foo
foo_l : Lens Foo Bool
foo_l =
Lens .foo (\f x -> { x | foo = f })
bar_p : Prism Bar Foo
bar_p =
Prism (\(Bar b) -> Just b) Bar
barFoo_o : Optional Bar Bool
barFoo_o =
Optional.composeLens (Optional.fromPrism bar_p) foo_l
x : Bar
x =
Bar { foo = True }
-- Using
barFoo_o.getOption x
--=> Just True
barFooo.set False x
--=> Bar { foo = False }