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
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
Both of these requests could definitely be covered by a more generic function.
plusOnePoly :: a -> a
plusOnePoly a = a + 1
Unfortunately, the code above doesn’t compile.
No instance for (Num a) arising from a use of '+'
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
, andRead
.
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
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
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]
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]
}
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"]
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
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)
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
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]
}
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.
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)
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 (==) | (/=) #-}
...
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
Ord
and Eq
go hand in hand in Haskell since Eq
is a superclass of Ord
.
class Eq a => Ord a where
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
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
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)
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"]}]
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
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
It offers the following functions:
-
compare
, which compares two values and gives anOrdering
, which is one of three values:LT
,EQ
, orGT
. - Operators for comparison:
<
,<=
,>
,>=
that take two values and return aBool
. -
max
andmin
, 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
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.
Top comments (0)