Type classes
Originally posted on dev.to/gillchristian.
Here we go with another chapter of the Haskell book from Delft Haskell Study Group Session #4. This time it was chapter 6, about Type classes.
As always, quotes are from the book and meant as a recap.
Type classes?
Type classes and types in Haskell are, in a sense, opposites. Where a declaration of a type defines how that type in particular is created, a declaration of a type class defines how a set of types are consumed or used in computations.
Type classes allow us to generalize over a set of types in order to define and execute a standard set of features for those types.
Type classes are sort of interfaces than can work on several types. Through type classes Haskell achieves constrained or ad-hoc polymorphism.
The goal is to define a datatype by cases, where one can add new cases to the datatype and new functions over the datatype, without recompiling existing code, and while retaining static type safety (e.g., no casts). 1
So we have type classes as constraints or rules and we say a type implements or instantiates the type classes by, well, implementing or instantiating those constraints/rules.
This is, for example, what the Bool
type implements.
λ> :i Bool
data Bool = False | True -- Defined in ‘GHC.Types’
instance Eq Bool -- Defined in ‘GHC.Classes’
instance Ord Bool -- Defined in ‘GHC.Classes’
instance Show Bool -- Defined in ‘GHC.Show’
instance Read Bool -- Defined in ‘GHC.Read’
instance Enum Bool -- Defined in ‘GHC.Enum’
instance Bounded Bool -- Defined in ‘GHC.Enum’
That means that all the constraints defined for Eq
, Ord
, Show
, Read
, Enum
and Bounded
are available to be used with Bool
s.
Let's take a look at Ord
λ> :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
{-# MINIMAL compare | (<=) #-}
-- ...
We can see it defines all ordering related operations, and also a minimal set of operations that need to be defined in order to instantiate the type class.
It's also constrained by Eq
, the type class for equality. Which makes sense, how can we order things if without knowing when they are equal or not.
λ> :i Eq
class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
{-# MINIMAL (==) | (/=) #-}
-- ...
By this definition we can see the constraints. All the a
s aren't fully polymorphic, they are constrained by the type class (Eq
or Ord
).
λ> :t (==)
(==) :: Eq a => a -> a -> Bool
Which can be any of the types that implement the type class.
Once we apply an argument it becomes more concrete:
λ> :t (==) 1
(==) 1 :: (Eq a, Num a) => a -> Bool
Prelude
λ> :t (==) (1 :: Int)
(==) (1 :: Int) :: Int -> Bool
Writing type classes
data DayOfWeek
= Mon
| Tue
| Wed
| Thu
| Fri
| Sat
| Sun
data Date =
Date DayOfWeek Int
instance Eq DayOfWeek where
(==) Mon Mon = True
(==) Tue Tue = True
(==) Wed Wed = True
(==) Thu Thu = True
(==) Fri Fri = True
(==) Sat Sat = True
(==) Sun Sun = True
(==) _ _ = False
instance Eq Date where
(==) (Date weekday dayOfMonth)
(Date weekday' dayOfMonth') =
weekday == weekday' && dayOfMonth == dayOfMonth'
Implementing type classes is quite straightforward, we have to define the required functions (the rules) for our type and that's it.
When writing instances of types with polymorphic parameters (like Date
from the example) we need to make sure those parameters also are constrained.
The following would result in a compile error.
data Identity a =
Identity a
instance Eq (Identity a) where
(==) (Identity v) (Identity v') = v == v'
It does not work because a
is fully polymorphic (i.e. not constrained), but in order to compare it it needs to implement Eq
.
instance Eq a => Eq (Identity a) where
(==) (Identity v) (Identity v') = v == v'
Making a
constrained by Eq
solves the problem 💪
We could also add Ord
as a constraint, since it also requires Eq
:
instance Ord a => Eq (Identity a) where
(==) (Identity v) (Identity v') = v == v'
Although that would work, it is not good to require more than is needed. Since we are not using any Ord
functions we are better off with Eq
.
For some type classes we do not need to write them ourselves, the compiler is smart enough to derive them.
data DayOfWeek
= Mon
| Tue
| Wed
| Thu
| Fri
| Sat
| Sun
deriving (Eq, Show)
Dispatched by type
Type classes are defined by the set of operations and values all instances will provide. Type class instances are unique pairings of the type class and a type. They define the ways to implement the type class methods for that type.
The class definition doesn’t define any terms or code we can compile and execute, only types. The code lives in the instances.
This means the compiler needs to know what types are involved in the operation defined by a type class and then use the implementation defined by those types. Haskell being lazy will delay this as much as possible.
Conclusions
Type classes are Haskell's approach to polymorphism and one of it's killer features. Not only it avoids inheritance problems! 🎉 but type classes are a very powerful way of constraining operations to certain types without coupling the types to the operations (like objects do).
The chapter mentioned a few more things about type classes, but this should already give a good practical idea of what type classes are.
P.S.: Don't know why I chose the pizza picture, but now you are hungry too.
Footnotes
-
Philip Wadler, “The Expression Problem” http://homepages.inf.ed.ac.uk/wadler/papers/expression/expression.txt ↩