It's been a while, but welcome back to yet another installment in our read-through of Haskell Programming from First Principles. This time we're looking at typeclasses.
What are typeclasses?
Typeclasses are like interfaces to your data. The description that I find the most intuitive is that a typeclass is a set of functions that must be implemented for your type to be an 'instance' of a typeclass. In classic OO-terms, this is very much like an interface; a contract that you must fulfill to have your type be able to stand in for this interface. In this way, typeclasses are a vehicle for ad-hoc or constrained polymorphism, and let you write code that operates on a more generic set of types than concrete implementations.
Typeclasses allow us to generalize over a set of types and to write functions that can abstract over concrete implementations. For instance: if all a function does with one of its input parameters is test it for equality, then the Eq
typeclass contains all the functionality we need, and we can make that function take any type that implements the Eq
typeclass. Similarly, all numeric literals and their various types implement the typeclass Num
, which defines a set of standard numeric operations, allowing functions to work on any numeric value.
To get a better feel for how this works, let's look at some of the basic typeclasses and some implementations!
Eq
Eq
, the typeclass for equality. A very simple typeclass that will let you decide whether two items are the same or not.
The definition is as follows:
class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
Pretty simple, right? All it needs is a definition for (==)
and one for (/=)
, and even better, the minimal complete definition --- that is, the least we need to implement to have a complete definition --- is just one of the two functions. The one we don't define will default to being the negation of the one we do define.
Most data types you'll work with implement this one already, so you don't need to worry much about it. Interestingly, because Haskell deals with structural equality, whether a tuple is an instance of Eq
or not, depends on its components. If all the components can be compared for equality, then so can the tuple. This also applies to lists and a range of other data structures.
Writing an instance of the Eq
typeclass
As an aside, here's how you'd write your own instance of the Eq
typeclass. It's a simple instance to write, but that makes it easier to learn from. Let's look at a basic example using a simple type we define, Bool2
:
data Bool2 = No | Yes
instance Eq Bool2 where
Yes == Yes = True
No == No = True
_ == _ = False
So to create an instance, we put instance
, the name of the typeclass, the name of the type to implement it for, and the where
keyword. What follows is an implementation of the functions required for the typeclass. As mentioned above, the minimal complete definition of the Eq
typeclass only requires one of the functions to be defined, so this is a valid implementation.
Num
The Num
typeclass is one we've already looked at quite a bit previously, so we're not going to dwell on it for too long. It's a typeclass implemented by most of the numeric types, and the definition is as follows:
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
Fractional
There are various other typeclasses related to Num
, but let's look at Fractional
, a subtype of Num
:
class (Num a) => Fractional a where
(/) :: a -> a -> a
recip :: a -> a
fromRational :: Rational -> a
This typeclass requires its type argument, a
, to have an instance of Num
. This means that a Fractional
is a specialized Num
and can be used anywhere a Num
can, but also specifies an extra set of functionality that is not required of the regular Num
typeclass.
Ord
This typeclass takes care of ordering things, and as such, has a definition concerned with orders and comparisons:
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
Note that it has a typeclass constraint of Eq
. If you're going to be comparing things, you're going to have to be able to tell whether they are equal or not. These functions all work the way you'd expect them to.
Ord instances (when derived automatically) rely on the way the data type is defined. Looking back on our Bool2
data type from earlier (data Bool2 = No | Yes
), Yes
will be greater than No
because it is defined later. This is similar to how enums work in the C-familiy of languages where they're usually just fancy names for successive integers starting at an arbitrary point and incrementing by one for each entry. Naturally, if you want to change the ordering, you can, but you're going to have to implement the typeclass yourself.
Enum
The Enum
typeclass is quite different (yet weirdly similar) to most other things known as enums in other programming languages (at least in my experience). In Haskell, the Enum
typeclass is for types that are enumerable, that have known predecessors and successors. What does this mean? It means you can use instances of this typeclass to generate ranges and lists.
Definition:
class Enum a where
succ :: a -> a
pred :: a -> a
toEnum :: Int -> a
fromEnum :: a -> Int
enumFrom :: a -> [a]
enumFromThen :: a -> a -> [a]
enumFromTo :: a -> a -> [a]
enumFromThenTo :: a -> a -> a -> [a]
The most obvious instance of this typeclass would be something like integers, where succ 4
is 5
and pred 3
is 2
. Similarly, characters can be enumerated just as well, as can ordered data types like Bool
.
Some of the function names listed above are a bit confusing (enumFromThenTo
anyone?), but when you look at it in context it makes more sense. For instance,
enumFromThenTo 1 10 100
-- yields [1,10,19,28,37,46,55,64,73,82,91,100]
Where this becomes more interesting is when you see that you can use them in list comprehensions and figure out that there's alternative ways of using these methods. Here's the equivalent written as a list comprehension instead:
[1, 10 .. 100]
-- yields [1,10,19,28,37,46,55,64,73,82,91,100]
Show
Show
is a typeclass that provides human-readable string-representations of structured data. GHCi uses this to create String
values that it can print in the terminal. In fact, to have a type be printed in the terminal, it must implement Show
.
class Show a where
showsPrec :: Int -> a -> ShowS
show :: a -> String
showList :: [a] -> ShowS
Unless you're writing this for your own data structures, you don't need to worry much about this one. It's already implemented for all basic types. The minimal complete definition requires either show
or showsPrec
.
Definitions
We've covered a lot of ground here, so let's take a step back and look at some definitions for this chapter.
- Typeclass inheritance
- When a typeclass has a superclass. This means that only data types that implement the super class can have an instance of the subclass. Going back to the
Num
andFractional
typeclasses: EveryFractional
is also aNum
, but not everyNum
is aFractional
. This makesNum
a superclass ofFractional
. - Instance
- An instance is the definition of how a typeclass should work for a given type. Instances must be unique for a given combination of typeclass and type, i.e. you cannot have two different instances of a typeclass
T
for a single typea
. - Derived instances
- Some typeclasses are so obvious or common that they can be automatically implemented for a data type. Automatically deriving typeclasses like this is done by specifying it after the data declaration:
data MyType = A | B deriving (Eq, Show)