How to do enums in Elm


#1

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.


#2

Very good.

With reference to “the expression problem”, tagged union types are complete and not dynamically extensible so they cannot be the right way to approach this. The way to make code behaviour extendable is always to pass a function. I notice that you have this in your functions where you pass in a (a -> String):

findEnumValue : List a -> (a -> String) -> String -> Maybe a
findEnumValue enum enumToString value =
    enum
    |> List.filter ((==) value << enumToString)
    |> List.head

If you wanted to capture the whole Enum type as a single thing, you could do this:

type alias Enum a = 
   { values : List a
   , toString : a -> String
   }

Then all your function signatures would be in terms of enums:

findEnumValue : Enum a -> String -> Maybe a
decodeEnumValue : Enum a -> String -> Decoder a
onEnumInput : Enum a -> (a -> msg) -> Attribute msg

#3

Nice rundown of the pros and cons of strings vs enums! I enjoyed following along with your process as you implemented the string-based alternative :smiley:

I may be an outlier but I don’t mind writing serialization/deserialization logic for my types (including enums) :open_mouth:. It makes sense (to me) that data needs to be serialized/deserialized at the edges of the system. This includes HTML forms or JSON sent over the wire.

I typically define

toString : MyType -> String
fromString : String -> Result String MyType

and often a decoder like:

decoder : Decoder MyType
decoder =
  JD.string |> JD.andThen (JD.fromResult << fromString)

The idea of having both fromString and decoder be derivable given a list of values and a toString function is fascinating :thinking:. This should be possible with union type enums too. So far in my code though, these functions have been simple/short so I haven’t had pain pushing me to abstract but I can see why that might be appealing.


#4

Thanks for the comments. :smiley:

I agree with your Enum type alias approach. It improves readability quite a bit!


#5

Thanks Joel! :smiley:

Good point on the union type enums - it should work fine with them too.


#6

One thing to be aware of, if you use that approach you may end up with functions in your model - which works just fine, but some people try to avoid doing it.


#7

Functions are problematic for the == operator, a comparison of functions or a record that has functions inside can cause a runtime exception. Also, records should more often than not be serializable data just for the sake of people’s sanity (and Debug.log). If you put functions inside the records then the record contains logic, which is kind of against pure functions and data argument. Don’t get me wrong, I can totally see the utility in this approach but it has downsides and I probably wouldn’t use that outside of pet projects :woman_shrugging:


#8

All good points :+1:, I have some thoughts around this but will save for another thread so as not to derail the enums discussion.


#9

It’s not clear to me that using the type alias approach would end up with functions in the model.

If I understand it correctly, the values here is not some subset of a, but instead is all the possible values of a. So, it doesn’t change over time, and can be defined (for a given a) once, as a singleton. It doesn’t look like something you’d put in your model.

You might have an a in your model, or even a List a, but that’s a different matter.


#10

It wasn’t clear to me either, so I said it may end up in the model. But I think you are right, this definition only really needs to go at the top-level for an enum.

Also I can’t see why we would need to to enum1 == enum2, but if we really did we could do enum1.values == enum2.value, and also Debug.log "enums" enum.values, so I guess bundling them together does not really have to be a problem.


#11

somebody build the enum package already :slight_smile:


#12

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.