Why type classes aren’t important in Elm yet

Pretext

Noah
9 min readMar 4, 2017

These are a series of posts talking about solving some of the problems with Elm that I see a lot in the community, and some discussion points around those problems. If you don’t come away thinking about a new way to solve a problem, then my post has failed you!

Elm does not currently support typeclasses, nor functions that can take arguments based on a contract. It often comes up in the community as a question — “when will Elm get typeclasses?”. This post aims to explore why people want typeclasses, cases where they might be used in Elm today, and the reason why I believe that typeclasses are important, but not a priority.

What’s the problem?

The main discussion is that Elm should have a feature known as typeclasses. I’ll explain typeclasses a bit further down for those unfamiliar with them. Haskell is a language which has promoted the use of typeclasses. Elm sits in a funny place, between the world of wat-typed JS, and the burrito-typed Haskell. I originally come from more of the Haskell background, but I can write JS very well these days, if I do say so myself.

What about Elm users? Where do they come from? Well, luckily for us, Brian Hicks collected some data on this.

71% of Elm developers either have basic or no familiarity with FP

It turns out that the majority of Elm developers are either not at all familiar with FP, or have basic familiarity with the concepts. On the other hand, there’s a large portion of people who come from Haskell and are deeply familiar with monads. That’s quite a mix up there.

When any discussions of typeclasses come up, I think it’s important to remember this. Elm is a language aimed primarily at Javascript developers or people with little FP experience. That doesn’t mean that more powerful features can’t be added, but it does mean that they need to be thought about very carefully.

What is a typeclass?

There’s a lot of reading about what a typeclass is, so I won’t go into it too much detail here. The tl;dr is that people want to be able to say that a data type has some standard behaviour, and then write a single function that will then work with any data type that follows that behaviour. Here’s an example:

interface CanBeAnInt a =
toInt : a -> Int

type AnimalEnum = Cat | Dog
toInt : AnimalEnum -> Int
toInt animal =
case animal of
Cat -> 0
Dog -> 1
type DirectionEnum = Up | DowntoInt : DirectionEnum -> Int
toInt direction =
case direction of
Up -> 1
Down -> -1
addThingsUp : List (CanBeAnInt a) -> Int
addThingsUp things =
List.map toInt >> List.sum

Here, the function addThingsUp can take either our DirectionEnum or our AnimalEnum. This means that we only need to write out addThingsUp once. In current Elm, this is how you’d do the same thing:

-- Animal.elm
type AnimalEnum = Cat | Dog
toInt : AnimalEnum -> Int
toInt animal =
case animal of
Cat -> 0
Dog -> 1
addThingsUp : List AnimalEnum -> Int
addThingsUp things =
List.map toInt >> List.sum
-- Direction.elm
type DirectionEnum = Up | Down
toInt : DirectionEnum -> Int
toInt direction =
case direction of
Up -> 1
Down -> -1
addThingsUp : List DirectionEnum -> Int
addThingsUp things =
List.map toInt >> List.sum

Notice that you have to define the exact same function twice.

Redefinitions in the wild

At the time of writing, map has been defined 104 times in public Elm packages[0]. This sounds like a lot, but a common problem people claim is “now I have to remember which map function to use!”. Well, let’s take a look at some code:

In Elm, we prefer to keep our data structures namespaced by their name. For example, it’s very rare to import map globally, and instead use List.map or Dict.map. This is both a solution and a symptom — without typeclasses, you cannot get the compiler to guess which implementation of map to use. In fact, if you import two symbols with the same name, Elm will complain at you.

With that being said, the majority of people are not defining their own data structures. Elm generally believes in following what I like to call “there’s only one library to do it”. Want a dict? You’re probably either using core’s Dict, or elm-all-dict. Want to test things? You’re using elm-test. And so on, for most types of problem, there’s a defacto library.

This is pretty handy when it comes to developing things. I never have to spend long looking through packages to decide which one to use. It also means that if I have a problem with a library, I will probably reach out to the creator of that packages instead of implementing my own.

You might say “what if I need a dict that performs better in this situation??” and that’s totally fair. Except, most of the time, you don’t need to worry about performance. Elm is targeting client side applications, and that’s what it’s optimised for. Your code is more likely to be slow due to your algorithms than it is the data structure. I enjoy pushing Elm the limits of what it can do, and I’ve found very few limits on how it performs. When it is slow, it’s because I need to apply lazy to a view, or I’ve been using it for server-side code.

A solid use case

Typeclasses are particularly useful for data structures that require the user to implement some kind of function. Dictionaries in Elm are implemented as red-black tree. What this means is that Elm uses the comparison of two values in order to discover which is smaller, and place it in the right place. This is why you can only have a dict of comparable. AllDict works around this limitation, by requiring you to pass in a function used for figuring out which element is smaller.

AllDict’s empty function takes a way of turning a key into a comparable

Typeclasses would help solve this problem, by instead saying that your data must look like this:

empty : Dict (Ord k) v

meaning that k must have an orderable function defined. Right now, you can use toString to achieve the same thing — but that is not ideal for collision reasons.

In fact, the comparable type is actually a typeclass defined inside Elm, that can not be extended or changed in any way. The same goes for appendable and number. Number is the simplest to explain — it contains only Int and Float, and is how you can do 4 + 5and 4.5 + 5.5 using a single operator. So you can see that Elm has some use cases for typeclasses, and accepts that, but introducing typeclass syntax into Elm is a heated discussion that occurs weekly. You can read the Github issue here for further insight into this discussion.

How production code looks

The majority of Elm code in production looks the same. We have our views, our entry points (where main is defined), our models, and our update code. Depending on the domain, you may have a lot of models. You might even have more models than anything else. Here’s an example of some models you might see in example code:

type alias City = { businesses : List Business }
type alias Business = { people : List Person }
type alias Person = { pets : List Pet }
type alias Pet = { species: Animal }
type Animal = Cat | Dog
viewAnimal : Animal -> Html msg
viewAnimal animal = toString animal |> Html.text
viewPerson : Person -> Html msg
viewPerson person =
List.map (.species >> viewAnimal) person.pets
|> Html.div []
viewBusiness : Business -> Html msg
viewBusiness business =
List.map viewPerson business.people
|> Html.div []
viewCity : City -> Html msg
viewCity city =
List.map viewBusiness city.businesses
|> Html.div []

Here we have 4 different types of records, and then one union type we’re using as an enumeration of animal species. We also have 4 different view functions, defined just to create a pure text representation of each type of record. Notice how the implementation for viewBusiness and viewCity is identical, other than the fields it works with.

We can cut down on the amount of view code we have, by doing something like this:

viewSomething : 
(record -> List field)
-> (field -> Html msg)
-> record
-> Html msg
viewSomething getter viewFunction record =
List.map viewfunction (getter record)
|> Html.div []

We define a function that first takes a function which takes a record and produces a list of fields. For our Business, we can just use .people, for example. We then take something that will convert each element of the field list into html. Finally, we take a record, then apply the two other functions and wrap everything in a div, which then means the rest of our view code looks like this:

viewAnimal : Animal -> Html msg 
viewAnimal animal = toString animal |> Html.text
viewPerson : Person -> Html msg
viewPerson person =
viewSomething (.pets) (.species >> viewAnimal) person
viewBusiness : Business -> Html msg
viewBusiness business =
viewSomething .people viewPerson business
viewCity : City -> Html msg
viewCity city =
viewSomething .businesses viewBusiness city

We didn’t really save much code there. I also think it’s a bit harder to see what each function should be. Could you tell what viewCity looks like without referring to the original functions? The introduction of type classes would allow you to define viewSomething in multiple ways, so that you could consistently use it across records that look like all kinds of things. For example, we could rewrite viewAnimal in order to just use viewSomething identity toString animal, or something to that effect.

If this was applied to all the codebase, the majority of Elm would be like discovering something new each time you changed code base. Right now, any Elm developer can examine Elm code that another developer has written, and fully understand what is going on. I consider this a major thing that Elm has got right.

One thing that is a common problem, however, is dealing with nested update loops. In order to call an update function from another function, you must manually unwrap the results, and set parts of the “top-level” model and map the effects. This is a problem that mainly exists due to the fact that you can not pass setters around freely. I consider this to be a bigger source of pain with a simpler fix than adding full-on typeclasses to a language.

Conclusion

Typeclasses help developers to keep track of pieces of data that behave in the same way as each other. This can not only help when thinking about problems, but it can also help developers solve problems at a higher level of abstraction that they might be able to otherwise.

Thinking about how to introduce typeclasses into Elm is a problem that requires a lot of time and design thinking. Simply adding Haskell style typeclasses is not the Elm approach. Elm prefers to reduce the number of ways to approach a problem, while typeclasses allow for multiple ways.

Elm is targeted at the main source of users it has — Javascript developers. Javascript developers may be familiar with typeclasses, but it is less likely that they are than Haskell developers. Alternatives like Purescript provide more abstractions and powerful type systems suited to those from the Haskell community. There are many of us Haskell users who sit happily with Elm too.

The majority of production code written in Elm would not benefit as much from typeclasses as they could from other features. This is why I am of the belief that introducing type-classes right now into Elm would not answer the problems people are facing. There are bigger priorities that should be focused on, such as scaling update functions to behave correctly. While typeclasses can help there, there is other, simpler ways to solve the same problem (such as allowing for setters to be generated for you). But that’s for another blog post.

Post text

[0] — I figured this out by using http://klaftertief.github.io/elm-search/?q=map then running this code:

--

--

Noah

Most of what I make are experiments. I promote both the ideas of getting things done and getting things done the right way.