TL;DR: I’ve made a working Ellie for this if you want to play around: https://ellie-app.com/nt22pfXCtKa1
I’ve seen a few comments around how working with enums suck in Elm. I believe much of the frustration comes from trying to re-implement in Elm a language feature that exists in other languages. As a result of applying the wrong kind of thinking, the wrong abstraction is reached and Elm gets the blame. So while I’m no expert, the following approach may lessen the pain that some people feel.
Let’s start with an example of the wrong way to do things, that, unfortunately, I have seen repeated often, e.g.
hackernoon: a-small-dive-into-and-rejection-of-elm-8217fd5da235 and http://reasonablypolymorphic.com/blog/elm-is-wrong/
Elm has union types, so we can model an enum like this:
type Preference = NoPreference | Morning | Afternoon`
We need to show these values in a select list, so we need a text description of each preference option.
displayName preference =
case preference of
NoPreference -> "No preference"
Morning -> "Morning"
Afternoon -> "Afternoon"
There’s no way to enumerate the values of a Preference, so we also need a list
preferences = [ NoPreference, Morning, Afternoon ]
Now we build our select list
preferenceOption preference = option [] [ text <| displayName preference ]
Using this we can display a select list to a user. Cool! Ok, but how do we get the value our user has selected? We can use the onInput function, but that produces a msg that has a string, so we need a function to convert the string to our Preference type for our msg.
type Msg = PreferenceSelected Preference
parsePreference string =
case string of
"Morning" -> Morning
"Afternoon" -> Afternoon
_ -> NoPreference
view model =
select [ onInput (PreferenceSelected << parsePreference) ]
(List.map preferenceOption preferences)
At this point everyone gives up because Elm is so repetitive, doesn’t have typeclasses, forces us to break type safety (with the _) etc. etc. etc.
Let’s take a deep breath and start again. What is an enum anyway? Some languages have special keywords for this, Elm doesn’t, but what is it? Well, an enum is a list of values. Pretty simple. How should this be modelled in Elm?
List String
Yes, that’s it. Elm already has a way of modelling a list of values and we can get away with the bare minimum of code when using Elm’s built in List type. Continuing our example we have the following:
preferenceEnum = [ "No preference", "Morning", "Afternoon" ]
type Msg = PreferenceSelected String
-- View
preferenceOption preference = option [] [ text preference ]
view model =
select [ onInput PreferenceSelected ]
(List.map preferenceOption preferenceEnum)
I would wager that the approach above would be enough for some. Yes, I know that using a string isn’t particularly type safe, but we need to understand our domain a lot more before we do anything more advanced than this. Using a type safe language does not mean we have to hit type safe perfection before we can get any work done.
For the sake of argument, lets see if we can improve the type safety a little bit. One thing we could do is tag our string
type Preference = Preference String
If you’re worried about rogue code generating values of this type, you could hide the constructor in a module and make Preference an opaque type. Either way, the definition of the enum changes slightly:
preferenceEnum =
[ Preference "No preference"
, Preference "Morning"
, Preference "Afternoon"
]
Notice we’re still using a list as the core concept of what an enum is: a list of values. We’re fiddling around with what the values are but the enum is still essentially the same thing.
For display purposes we’ll need a helper function to get our string back out.
preferenceToString preference =
case preference of
Preference string -> string
Or as I’ve seen written elsewhere:
preferenceToString (Preference preference) = preference
And the rest of the code has minor changes too:
type Msg = PreferenceSelected Preference
preferenceOption preference = option [] [ text <| preferenceToString preference ]
select [ onInput PreferenceSelected ]
(List.map preferenceOption preferenceEnum)
All good. Almost. We’ve broken our select handler by using our tagged type rather than a string. Instead of jumping straight into writing a decoder, the reverse of preferenceToString
, we should stop and think for a bit. If an enum is a list of values, and those values have unique string representations (directly or with a toString
function), then we should be able to get back the value from the string using a reverse lookup rather than a new custom decoder for every enum that we could write.
findEnumValue : List a -> (a -> String) -> String -> Maybe a
findEnumValue enum enumToString value =
enum
|> List.filter ((==) value << enumToString)
|> List.head
Notice that this is rather generic and doesn’t care what values are in the enum. Writing a decoder from this is quite straightforward:
decodeEnumValue : List a -> (a -> String) -> String -> Decoder a
decodeEnumValue enum enumToString stringValue =
case findEnumValue enum enumToString stringValue of
Just value ->
Decode.succeed value
Nothing ->
Decode.fail <| "Could not decode value to enum: " ++ stringValue
To round things off we could implement a generic enum input handler:
onEnumInput : List a -> (a -> String) -> (a -> msg) -> Attribute msg
onEnumInput enum enumToString tagger =
let
decodeTargetValue =
targetValue
|> Decode.andThen (decodeEnumValue enum enumToString)
|> Decode.map tagger
in
on "input" decodeTargetValue
Now we’re done! If you really really wanted to, you could return to the definition of Preference
at the start of this post, but the way forward is clear. Simply change your implementation of preferenceToString
and the list of values in your enum and that’s it.