Many faces with different expressions.

Haskell, in Elm terms: Type Classes

terezka
7 min readOct 16, 2019

--

I’ve been working with Elm for many years now, but only recently have I begun to learn Haskell for work. I often found myself confused by the explanations for even the most fundamental concepts, despite what I’m sure were well-meaning intentions. This was especially true when the explanations were termed in what to me was estranging academic vocabulary. Here’s my attempt at a blog post which I wish could have been referred to.

The Grand Divide

On the borders between the Elm and Haskell community, the value of type classes is often discussed (Probably too often!). From the conversations, it sounded to me like Haskell had type classes and Elm didn’t, so it came as a surprise to me when I found that Elm in fact does have type classes, and I had used them all along.

Typeclasses in Elm

There are several type classes in Elm. One of them is number, used all around the core modules. In the Basics.elm module:

(+) : number -> number -> number
(-) : number -> number -> number
(*) : number -> number -> number
(/) : number -> number -> number
abs : number -> number
negate : number -> number

These annotations describe how the math functions cannot take arguments of any type, but only a select set. Intuitively, this makes sense: It’s obvious that Int’s and Float’s can be multiplied, but how would the compiler know how to add together two records which I defined? It can’t know. Thus Int’s and Float’s are categorized under the name number because they share the ability to be manipulated by arithmetic operators. They are a class of types which share a set of functions, hence the name “type classes”. Members of a type class are called “instances” of that type class, thus Int and Float can be called instances of the number type class.

Typeclasses in Haskell

But if type classes exist in Elm, what more did those Haskell developers want? The big difference is that in Haskell you can define type classes and their instances yourself. If the number type class from Elm was defined in Haskell, it could look something like this:

class Number a where
(+) :: a -> a -> a
(-) :: a -> a -> a
(*) :: a -> a -> a
(/) :: a -> a -> a
abs :: a -> a
negate :: a -> a

It tells the compiler that any instance of this type class must have these functions. Once you have defined your type class, you can add any type to this class, as long as you define the required functions, too.

type Person =
{ name :: List Char
, age :: Int
}
instance Number Person where
(+) :: Person -> Person -> Person
(+) a b =
Person (take 4 a.name ++ drop 1 b.name) (a.age + b.age)
(-) :: Person -> Person -> Person
(-) a b =
-- ... more silly implementations

With Person defined as an instance of Number we can now use the arithmetic operators on people.

example :: Person
example =
Person "Tereza" 23 + Person "Evan" 29 -- Person "Terevan" 52

This is of course a silly implementation, because treating people like numbers doesn’t make any sense, but anything is possible in Haskell! Depending on who you ask, that is a blessing or a curse.

An aside on type constraints

Once you have defined a type class in Haskell, you (and anyone else) can create additional functions only accessible by types which are instances of your type class. This feature is called “type constraints”. Given our Number type class in Haskell, one could add a sum function like so:

sum :: (Number a) => List a -> a
sum as =
foldl (+) 0 as

A sum function does not make sense unless its arguments can be treated like numbers. Thus, by adding (Number a) => in the beginning of the signature, we constrain all mentions of a in the rest of the signature to be instances of the Number type class. This is important because we need to use the (+) operator in the definition, which is only accessible to instances of the Number type class!

The point of type classes

The number type class was actually the very first type class to be introduced. It’s a neat party trick because we often have Float and Int within the same scope and without type classes, we’d be required to have separate modules for Float and Int, each containing all of the arithmetic operators, and then qualifying those arithmetic operators upon usage:

-- Without type classesexample :: Int
example =
3 Int.(+) 5

-- With type classes
example :: Int
example =
3 + 5

This illustrates the essence of the feature: Type classes remove the need for qualifying functions. So if you don’t like qualifying your functions, having many type classes is your blessing.

Common type classes

As it turns out, Haskell does not like qualifying functions, hence it has a lot of type classes. In fact, if we try to find all the functions which share a name across modules in Elm, they will likely be type classes in Haskell.

Map

One of the first functions to come to mind may be the commonly occurring map, as seen in many of the core modules, e.g. the Maybe and List module:

-- Maybe.elm
map : (a -> b) -> Maybe a -> Maybe b
-- List.elm
map : (a -> b) -> List a -> List b

and indeed, there is a type class for it in Haskell.

class Functor f where
fmap :: (a -> b) -> f a -> f b

Consider the type signature of fmap: If you replace the generalized f type variable with Maybe or List, you’ll see that it’s the exact same signature as that of the map functions in Elm! This means that in Haskell, you don’t have to qualify your maps, you use fmap, as shown in the following snippet.

-- Elmexample : Maybe Person
example =
Maybe.map (Person "Tereza") (Just 23)

-- Haskell
example :: Maybe Person
example =
fmap (Person "Tereza") (Just 23)

This type class is called the “Functor” type class, so when people say a type is a “functor”, it just means it’s mappable.

Map2

Another example may be that of map2 as seen in the Maybe and Result core modules:

-- Maybe.elm
map2 : (a -> b -> c) -> Maybe a -> Maybe b -> Maybe c
-- Result.elm
map2 : (a -> b -> c) -> Result x a -> Result x b -> Result x c

These functions do not translate as literally into Haskell as map did, but it does have an equivalent, merely a little manipulated. Take a look at the type class below:

class Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b

For the sake of this explanation, we will will look at the implementation of this type class in the case of Maybe, resulting in the following functions:

pure :: a -> Maybe a
pure a =
Just a
(<*>) :: Maybe (a -> b) -> Maybe a -> Maybe b
(<*>) maybeFunc maybeA =
case ( maybeFunc, maybeA ) of
( Just func, Just a) ->
Just (func a)
_ ->
Nothing

The exciting part here is that from these two functions combined, you can make any map you like:

map2 :: (a -> b -> c) -> Maybe a -> Maybe b -> Maybe c
map2 func maybeA maybeB =
pure func <*> maybeA <*> maybeB

If you’re anything like me, this might be tying a knot in your brain, so here are the types in progressing order, but don’t worry if you have to play if over a few times.

pure     -- a -> Maybe a
(<*>) -- Maybe (a -> b) -> Maybe a -> Maybe b
func -- a -> b -> c
pure func -- Maybe (a -> b -> c)
pure func <*> maybeA -- Maybe (b -> c)
pure func <*> maybeA <*> maybeB -- Maybe c

Likewise, you can make map3 by tagging another <*> on to the end:

map3 :: (a -> b -> c -> d) -> Maybe a -> Maybe b -> Maybe c -> Maybe
map3 func maybeA maybeB maybeC =
pure func <*> maybeA <*> maybeB <*> maybeC

And so on. This type class is in Haskell called the “Applicative” type class, meaning that when Haskell people say that a type is an “applicator”, they mean that it can be chained together using the <*> operator.

If you’re curious about the application of this technique in Elm, it is used in elm-json-decode-pipeline, even if a little stealthily. That’s why it does not have the map, map2, map3… pattern.

andThen

Another example of functions that often occur in Elm API’s, may be that of andThen as seen in the Maybe and Result core modules:

-- Maybe.elm
andThen : (a -> Maybe b) -> Maybe a -> Maybe b
-- Result.elm
andThen : (a -> Result x b) -> Result x a -> Result x b

Once again, this is also a type class in Haskell!

class Monad m where
(>>=) :: m a -> (a -> m b) -> m b

And again, if you exchange m for Maybe or Result, you’ll see that the signature is the same as that of andThen, except the arguments are flipped. They are flipped in Haskell because their version is an infix operator.

-- Elmexample : Maybe Int
example =
Just "23" |> Maybe.andThen String.toInt
-- Haskellexample :: Maybe Int
example =
Just "23" >>= readMaybe

And that’s the infamous monad! Meaning that when Haskell people say a type is a “monad”, it just means andThen can be applied to it.

Conclusion

There isn’t actually anything you can do with type classes which you can’t do without them- it’s just a matter of how you prefer to use and talk about types and what abilities they have. Elm chose not to have custom type classes, because qualifying functions builds in hints in your code, helping you to keep track of what types you’re dealing with, and because the language is meant to be easily approachable to people who are not already familiar with functional languages. Haskell, as a research language, does not explicitly aim to be easily approachable, rather it is meant to be a playground for new and interesting features. Growing more familiar with Haskell, I think it’s wonderful to have both these worlds available.

--

--