DEV Community

Cover image for Introduction to Haskell Typeclasses
Serokell
Serokell

Posted on • Originally published at serokell.io on

Introduction to Haskell Typeclasses

Imagine you’ve been tasked to write a function for increasing a value by one in Haskell.

What’s easier? Finally, a place where one can use previous JavaScript experience. 😅

You find the type of bounded integers – Int – and get to work.

plusOne :: Int -> Int
plusOne a = a + 1

Enter fullscreen mode Exit fullscreen mode

But it turns out that the team lead also wants a plus function that works with floating-point numbers.

plusOneFloat :: Float -> Float
plusOneFloat a = a + 1

Enter fullscreen mode Exit fullscreen mode

Both of these requests could definitely be covered by a more generic function.

plusOnePoly :: a -> a
plusOnePoly a = a + 1

Enter fullscreen mode Exit fullscreen mode

Unfortunately, the code above doesn’t compile.

No instance for (Num a) arising from a use of '+'

Enter fullscreen mode Exit fullscreen mode

To sum two members of the same type in Haskell via +, their type needs to have an instance of the Num typeclass.

But what’s a typeclass, and what’s an instance of a typeclass? Read further to find the answers.

I’ll cover:

  • what typeclasses are;
  • how to use them;
  • how to create instances of typeclasses;
  • basic typeclasses like Eq, Ord, Num, Show, and Read.

Recommended previous knowledge: algebraic data types.

What’s a typeclass in Haskell?

A typeclass defines a set of methods that is shared across multiple types.

For a type to belong to a typeclass, it needs to implement the methods of that typeclass. These implementations are ad-hoc: methods can have different implementations for different types.

As an example, let’s look at the Num typeclass in Haskell.

class Num a where
  (+) :: a -> a -> a
  (-) :: a -> a -> a
  (*) :: a -> a -> a
  negate :: a -> a
  abs :: a -> a
  signum :: a -> a
  fromInteger :: Integer -> a

Enter fullscreen mode Exit fullscreen mode

For a type to belong to the Num typeclass, it needs to implement its methods: +, -, *, and so forth.

If you want to use one of its methods, such as +, you can only use it on types that have an instance of Num.

And a function that uses + needs to limit itself by only taking members of the Num typeclass. Otherwise, it won’t compile.

This is done by putting a type constraint (Num a =>) in the type signature.

plusOnePoly :: Num a => a -> a
plusOnePoly a = a + 1

Enter fullscreen mode Exit fullscreen mode

This stands in contrast to polymorphism across all types. For example, ++ will work with two lists of elements of the same type, no matter what that type is.

Prelude> :t (++)
(++) :: [a] -> [a] -> [a]

Enter fullscreen mode Exit fullscreen mode

Typeclasses are similar to Java interfaces, Rust traits, and Elixir protocols, but there are also noticeable differences.

What’s an instance of a typeclass?

A type has an instance of a typeclass if it implements the methods of that typeclass.

We can define these instances by hand, but Haskell can also do a lot of work for us by deriving implementations on its own.

I’ll cover both of these options in the section below.

How to define typeclass instances

Let’s imagine we have a data type for Pokemon that includes their name, Pokedex number, type, and abilities.

data Pokemon = Pokemon
  { pokedexId :: Int
  , name :: String
  , pokemonType :: [String]
  , abilities :: [String]
  }

Enter fullscreen mode Exit fullscreen mode

We have two Pokemon – Slowking and Jigglypuff – which are arguably the best offerings of the Pokemon universe.

*Main> slowking = Pokemon 199 "Slowking" ["Water", "Psychic"] ["Oblivious", "Own Tempo"]
*Main> jigglypuff = Pokemon 39 "Jigglypuff" ["Normal", "Fairy"] ["Cute Charm", "Competitive"]

Enter fullscreen mode Exit fullscreen mode

For some reason, we would like to know whether their values are equal.

Right now, GHCi cannot answer this.

*Main> slowking == jigglypuff

<interactive>:19:1: error:
    • No instance for (Eq Pokemon) arising from a use of '=='
    • In the expression: slowking == jigglypuff
      In an equation for 'it': it = slowking == jigglypuff

Enter fullscreen mode Exit fullscreen mode

That’s because Pokemon doesn’t have an instance of the Eq typeclass.

There are two ways of making Pokemon a member of the Eq typeclass: deriving an instance or manually creating it. I’ll cover them both.

Deriving Eq

When creating a type, you can add the deriving keyword and a tuple of typeclasses you want the instance of, such as (Show, Eq). The compiler will then try to figure out the instances for you.

This can save a lot of time that would be spent in typing out obvious instances.

In the case of Eq, we can usually derive the instance.

data Pokemon = Pokemon
  { pokedexId :: Int
  , name :: String
  , pokemonType :: [String]
  , abilities :: [String]
  } deriving (Eq)

Enter fullscreen mode Exit fullscreen mode

The derived instance will compare two Pokemon for equality by comparing each individual field. If all the fields are equal, the records should be equal as well.

Now we can answer our question.

*Main> slowking == jigglypuff
False

Enter fullscreen mode Exit fullscreen mode

Defining Eq

The Pokedex number should uniquely identify a Pokemon (if we use the National Pokedex). So, technically, we don’t need to compare all the fields of two Pokemons to know that they are the same Pokemon. Comparing just the index will be enough.

To do that, we can create a custom instance of Eq.

First, we need to remove the deriving (Eq) clause.

data Pokemon = Pokemon
  { pokedexId :: Int
  , name :: String
  , pokemonType :: [String]
  , abilities :: [String]
  }

Enter fullscreen mode Exit fullscreen mode

Then we can define an Eq instance for the Pokemon typeclass.

-- {1} {2}
instance Eq Pokemon where
  -- {3}
  pokemon1 == pokemon2 = pokedexId pokemon1 == pokedexId pokemon2

-- {1}: Typeclass whose instance we are defining.
-- {2}: The data type for which we are defining the instance for.
-- {3}: Method definitions.

Enter fullscreen mode Exit fullscreen mode

Even though Eq has two methods: == and /=, we only need to define one of them to satisfy the minimal requirements of the instance.

Minimal requirements to define an instance

It usually isn’t necessary to define all the methods of the typeclass.

For example, Eq has two methods: == and /=. If you define ==, it’s reasonable to assume that /= will be not ==.

In fact, that’s in the definition of the typeclass.

class Eq a where
    (==) :: a -> a -> Bool
    (/=) :: a -> a -> Bool
    x == y = not (x /= y)
    x /= y = not (x == y)

Enter fullscreen mode Exit fullscreen mode

For this reason, the minimal definition for Eq is to define one of these methods.

To see the minimal definition of a typeclass, you can use :info.

*Main> :info Eq
...
  {-# MINIMAL (==) | (/=) #-}
...

Enter fullscreen mode Exit fullscreen mode

If you provide the minimal implementation of a typeclass, the compiler can figure out the other methods.

This is because they can be:

  • defined in terms of methods you’ve already provided;
  • defined in terms of methods that a superclass of the typeclass has (I’ll cover superclasses later);
  • provided by default.

You might want to provide your own implementations for performance reasons, though.

Ordering Pokemon

Let’s imagine that we want to order and sort our Pokemon.

To compare two values of a type, the type needs to have an instance of the Ord typeclass.

class Eq a => Ord a where
  compare :: a -> a -> Ordering
  (<) :: a -> a -> Bool
  (<=) :: a -> a -> Bool
  (>) :: a -> a -> Bool
  (>=) :: a -> a -> Bool
  max :: a -> a -> a
  min :: a -> a -> a

Enter fullscreen mode Exit fullscreen mode

Ord and Eq go hand in hand in Haskell since Eq is a superclass of Ord.

class Eq a => Ord a where           

Enter fullscreen mode Exit fullscreen mode

Let’s quickly go over superclasses so that we understand what that means.

Superclasses

In Haskell, typeclasses have a hierarchy similar to that of classes in OOP.

If a typeclass x is a superclass of another class y, you need to implement x before you implement y.

In our case, we needed to implement Eq (which we did) before we implement Ord.

Furthermore, typeclasses often depend on their superclasses for method definitions. So you need to be careful that you “mean the same thing” when you define both the typeclass and its superclass.

For example, the Ord typeclass depends on the Eq typeclass for defaults, so it is a rule of thumb to have them be compliant with each other.

This means that our Ord instance should order things in a way that a <= b and a >= b implies a == b. Yeah, Haskell can be like that sometimes. 😂


Since we used the Pokedex number to define equality, we also will use it to define order.

In the case of Ord, the minimal definition is <= or compare. We’ll define the first of these.

Here’s how the instance definition looks:

instance Ord Pokemon where
  pokemon1 <= pokemon2 = pokedexId pokemon1 <= pokedexId pokemon2

Enter fullscreen mode Exit fullscreen mode

At this point, it’s easy to see why our Ord instance needs to be compliant with our Eq instance. Since we provided only the minimal implementation, Haskell will use the method of Eq== – and our definition of <= to create implementations for < and >.

Now we can compare Pokemon.

*Main> jigglypuff < slowking
True
*Main> jigglypuff > slowking
False

Enter fullscreen mode Exit fullscreen mode

We can also create a third Pokemon and sort a list of Pokemon using the sort function.

To see the sorted list in GHCi, we need to derive the Show typeclass for our data type.

data Pokemon = Pokemon
  { pokedexId :: Int
  , name :: String
  , pokemonType :: [String]
  , abilities :: [String]
  } deriving (Show)

Enter fullscreen mode Exit fullscreen mode

And now we can see the results:

*Main> chansey = Pokemon 113 "Chansey" ["Normal"] ["Natural Cure", "Serene Grace"]
*Main> import Data.List

*Main Data.List> sort([chansey, jigglypuff, slowking])
[Pokemon {name = pokedexId = 39, "Jigglypuff", pokemonType = ["Normal","Fairy"], abilities = ["Cute Charm","Competitive"]},Pokemon {pokedexId = 113, name = "Chansey", pokemonType = ["Normal"], abilities = ["Natural Cure","Serene Grace"]},Pokemon {pokedexId = 199, name = "Slowking", pokemonType = ["Water","Psychic"], abilities = ["Oblivious","Own Tempo"]}]

Enter fullscreen mode Exit fullscreen mode

Basic Haskell typeclasses

Let’s look at the basic Haskell typeclasses that you have encountered while reading this article.

Eq

Eq provides an interface for testing for equality. It has two methods: == and /= for equality and inequality, respectively.

class Eq a where
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool

Enter fullscreen mode Exit fullscreen mode

The minimal definition for Eq is to either provide == or /=.

You can generally derive this typeclass while defining your data types.

Ord

Ord is a subclass of Eq that is used for data types that have a total ordering (every value can be compared with another).

class Eq a => Ord a where
  compare :: a -> a -> Ordering
  (<) :: a -> a -> Bool
  (<=) :: a -> a -> Bool
  (>) :: a -> a -> Bool
  (>=) :: a -> a -> Bool
  max :: a -> a -> a
  min :: a -> a -> a

Enter fullscreen mode Exit fullscreen mode

It offers the following functions:

  • compare, which compares two values and gives an Ordering, which is one of three values: LT, EQ, or GT.
  • Operators for comparison: <, <=, >, >= that take two values and return a Bool.
  • max and min, which return the largest and smallest of two values, respectively.

The minimal definition for the Ord typeclass is either compare or <=.

Show and Read

Show is a typeclass for conversion to strings. Read is its opposite: it’s the typeclass for conversion from strings to values. The implementations are supposed to follow the law of read . show = id.

For beginners, the show method will be important for debugging purposes. If you are working with GHCi and need to print out your custom types in the terminal, you need to derive Show. Otherwise, you won’t be able to print them.

One important thing that some beginners get confused about: these typeclasses are not supposed to be used for pretty-printing and parsing complex values. There are better tools for that.

Num

Num is the typeclass for numbers.

class Num a where
  (+) :: a -> a -> a
  (-) :: a -> a -> a
  (*) :: a -> a -> a
  negate :: a -> a
  abs :: a -> a
  signum :: a -> a
  fromInteger :: Integer -> a

Enter fullscreen mode Exit fullscreen mode

The minimal definition for Num includes: (+), (*), abs, signum, fromInteger, and negate or (-).

It offers all the arithmetic operations that you would expect to need when working with integers.

Conclusion

This introduction to typeclasses in Haskell covered what typeclasses are and how to create your own instances by deriving or defining them.

For further reading, we have a series called What’s That Typeclass that covers more advanced typeclasses. So far, we have posts about Monoid and Foldable, but more are to come.

If you want to get informed about new beginner-friendly Haskell articles, follow us on Twitter or subscribe to our mailing list via the form below.

Latest comments (0)