A typeclass defines how a set of types are consumed or used in computations.
Typeclasses allow us to generalize over a set of types in order to define and execute a standard set of features for those types.
If you are familiar with concepts in other programming languages, typeclasses are similar to interfaces.
Let's look at the data declaration for Bool
and which typeclasses it already has instances of:
Prelude> :info Bool
data Bool = False | True -- Defined in ‘GHC.Types’
instance Eq Bool -- Defined in ‘GHC.Classes’
instance Ord Bool -- Defined in ‘GHC.Classes’
instance Enum Bool -- Defined in ‘GHC.Enum’
instance Show Bool -- Defined in ‘GHC.Show’
instance Read Bool -- Defined in ‘GHC.Read’
instance Bounded Bool -- Defined in ‘GHC.Enum’
Each of these instances is a typeclass that Bool
implements, and the instances are the unique specifications of how Bool
makes use of the methods from that typeclass.
instance Bounded Bool
- Bounded for types that have an upper and lower bound.instance Enum Bool
- Enum
for things that can be enumerated.instance Eq Bool
- Eq
for things that can be tested for equality.instance Ord Bool
- Ord
for things that can be put into sequential order.instance Read Bool
- Read parses strings into other things.instance Show Bool
- Show
renders things into strings.Typeclasses have a hierarchy.
For example, all Fractional
numbers implement the Num
typeclass, but not all Num
are Fractional
.
All members of Ord
must be members of Eq
, and all members of Enum
must be members of Ord
.
In Haskell, equality is implemented using the Eq
typeclass.
Eq
defined:
-- not all listed, check for yourself :)
instance Eq a => Eq [a] -- Defined in ‘GHC.Classes’
instance Eq Word -- Defined in ‘GHC.Classes’
instance Eq Ordering -- Defined in ‘GHC.Classes’
instance Eq Int -- Defined in ‘GHC.Classes’
instance Eq Float -- Defined in ‘GHC.Classes’
instance Eq Double -- Defined in ‘GHC.Classes’
instance Eq Char -- Defined in ‘GHC.Classes’
instance Eq Bool -- Defined in ‘GHC.Classes’
There are several numeric types, there is Bool
, Char
, tuples, and much more.
Anytime that we are using data of these types, we are implementing the Eq
typeclass and therefore have generic functions we can use to compare their equality.
Examples of Eq
in action:
Prelude> 1 == 1
True
Prelude> 1 /= 1
False
Let's look at the information for (==)
and (/=)
:
(==) :: Eq a => a -> a -> Bool
(/=) :: Eq a => a -> a -> Bool
With this information, we know that they can be used for any type a which implements the Eq
typeclass.
You can write your own datatypes and typeclasses.
For example, to make a minimal valid Eq
instance, you must define either (==)
or (/=)
.
Let's make our own datatype; I will call it Trite
.
data Trite
= Trite
If we try to test equality on this newly created type, GHC will throw a type error:
Prelude> Trite == Trite
<interactive>:2:1: error:
• No instance for (Eq Trite) arising from a use of ‘==’
• In the expression: Trite == Trite
In an equation for ‘it’: it = Trite == Trite
We get this error because GHC can't find an instance of Eq
for our datatype Trite
.
Let's write our own!
data Trite
= Trite'
instance Eq Trite where
[1] [2] [3] [4]
Trite' == Trite' = True
[5] [6] [7] [8]
instance
beings a declaration of a typeclass instance. Typeclass instances allow you to tell Haskell how typeclasses should work for a particular datatype. Without this instance, we would not be about to test the values for equality.Eq
.Eq
typeclass for the Trite
datatype.Trite'
is the first argument to the (==)
function. We are defining ==
using infix notation here.(==)
in infix notation.Trite'
. Trite' == Trite'
which is True
.Now the compiler knows how to test this datatype for equality. I changed the example to use a single quote at the end of the data constructor because they don't have to have the same name. This was done only for readability purposes.
Let's try testing it out:
Prelude> Trite' == Trite'
True
We can make a less trite example. Let's create a Colors
datatype and a Creature
datatype. The Creature
datatype describes a creature and the Colors
datatype describes the color of the creature.
data Colors
= Red
| Orange
| Yellow
| Green
| Blue
| Indigo
| Violet
data Creature
= Creature Colors
instance Eq Colors where
(==) Red Red = True
(==) Orange Orange = True
(==) Yellow Yellow = True
(==) Green Green = True
(==) Blue Blue = True
(==) Indigo Indigo = True
(==) Violet Violet = True
(==) _ _ = False -- exhaustive check
instance Eq Creature where
(==)
(Creature color)
(Creature color') =
color == color'
First, we wrote Eq
for Colors
. Next, we wrote Eq
for Creature. We test for equality by seeing if all of their constituent values are equal.
Testing it:
Prelude> Creature Orange == Creature Orange
True
Prelude> Creature Red == Creature Blue
False
Writing an instance of a typeclass for something with polymorphic parameters
We sometimes need to require our argument or arguments to provide some typeclass instances for us in order to write an instance for the datatype containing them
data Example a
= Example a
instance Eq (Example a) where
(==) (Example e) (Example e') = (==) e e'
We know that e and e' are both of type a, but we don't know anything about a. We can't assume that a has an Eq
instance.
Because of this, we use a typeclass constraint in the instance declaration.
data Example a
= Example a
instance Eq a => Eq (Example a) where
(==) (Example e) (Example e') = (==) e e'
Now, we know that a has an instance of Eq
.
We have already used Num
several times, but to reiterate, Num
is a typeclass implemented by most numeric types.
Let's do the same thing with Num
as we did with Eq
and query its information.
class Num a where -- left out some stuff
(+) :: a -> a -> a
(-) :: a -> a -> a
(*) :: a -> a -> a
negate :: a -> a
abs :: a -> a
signum :: a -> a
fromInteger :: Integer -> a
-- Defined in ‘GHC.Num’
instance Num Integer -- Defined in ‘GHC.Num’
instance Num Int -- Defined in ‘GHC.Num’
instance Num Float -- Defined in ‘GHC.Float’
instance Num Double -- Defined in ‘GHC.Float’
We've seen most of this before so no surprises here.
Integral
Integral
is a typeclass that has the following definitions:
class (Real a, Enum a) => Integral a where
quot :: a -> a -> a
rem :: a -> a -> a
div :: a -> a -> a
mod :: a -> a -> a
quotRem :: a -> a -> (a, a)
divMod :: a -> a -> (a, a)
toInteger :: a -> Integer
The typeclass constraint (Real a, Enum b) =>
means that any type that implements Integral
must already have instances for Real
and Enum
typeclasses.
Fractional
Num
is a superclass of Fractional
. The Fractional
typeclass is defined as follows:
class Num a => Fractional a where
(/) :: a -> a -> a
recip :: a -> a
fromRational :: Rational -> a
Fractional
requires an argument a to have an instance of Num
.
This is an example of typeclass inheritance. Fractional
applies to fewer numbers than Num
does, and instances of the Fractional
class can use the functions defined in Num
, but not all Num
can use the functions defined in Fractional
. This is because nothing in the Num
definition requires an instance of Fractional
.
When you have a typeclass-constrained polymorphic value and need to evaluate it, the polymorphism must be resolved to a specific concrete type.
The concrete type must have an instance for all the required typeclass instances.
For example:
1 / 2 -- the result of this defaults to a Double
You must specify which typeclasses you want type variables to have if they are not concrete.
In this case, the typeclass constraint is superfluous because the types are concrete.
We can make types more specific:
let x = 10 :: Integer
let y = 4 :: Double
Types can be made more specific, but not more general or polymorphic.
We have previously noticed that Ord
is the typeclass that covers the types of things that can be put in order.
Let's take a look at a parsed version of the info:
Prelude> :i Ord
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
The functions that come standard in this class have to do with ordering. Some of them give a result of Bool
as we've seen in the past.
Remember Ord
instances cannot exist without also having an instance of Eq
.
This logically makes sense because you can't order things without the ability to check for equality.
Enum
is a typeclass that is similar to Ord
but slightly different.
Enum
covers types that are enumerable.
Enumerables have predecessors and successors.
You probably have a good idea about how to check typeclass information at this point so I'll leave that to you.
The two main functions that Enum
has are succ
and pred
.
Prelude> succ 2
1
Prelude> pred 2
3
Here are two other functions that are pretty neat: enumFromTo
and enumFromThenTo
Prelude> enumFromTo 1 5
[1,2,3,4,5]
Prelude> enumFromThenTo 1 10 100
[1,10,19,28,37,46,55,64,73,82,91,100]
Show
is a typeclass that provides for creating readable string representations.
When you ask GHCi to return the result of an expression and print it to the screen, you are indirectly invoking a function called print
.
Printable types are those that are instances of class Show
.
Creating an instance of Show
:
data Shape
= Circle
instance Show Shape where
show _ = "Circle"
Read
is the opposite of Show
.
I would recommend not using Read
because it doesn't always work and there are better alternatives in Haskell.
I will be going over better alternatives at a later date.
At the end of the day, you can choose to use it or not, but I wouldn't.
Don't just take my word for it; do your own research.
Typeclasses are defined by the set of operations and values all instances will provide.
Typeclass instances are unique pairings of the typeclass and a type.
Remember:
An introduction to typeclass metaprogramming - https://lexi-lambda.github.io/blog/2021/03/25/an-introduction-to-typeclass-metaprogramming/
Type Classes in Haskell - http://ropas.snu.ac.kr/lib/dock/HaHaJoWa1996.pdf
Haskell Book - https://haskellbook.com/