Typeclasses
Typeclasses allow the user to add constraints to universally quantified types:
The type of notEqual
states: for any type a
such that a
is an instance of the Eq
typeclass, given a pair of a
s, this will return a Bool
.
Note
Eq a
is not a type, but rather a different kind of entity, called a constraint.
Eq
is referred to as a typeclass.
Basics¶
Typeclasses, such as Eq
, are defined as follows:
- The actual definition has a second method,
(/=)
, omitted here for clarity.
Here, (==)
is a method of the Eq
typeclass.
To make a type be an instance of a type class, one writes a definition of the method(s) for the type in question:
Automatically deriving instances¶
> data Piece = Bishop | Knight deriving (Eq, Ord, Show)
> Bishop == Knight
False
> show Bishop
"Bishop"
> import Data.List
> sort [Knight, Bishop]
[Bishop,Knight]
Different instances for the same type¶
A single type can have at most one instance for a typeclass.
However, two types which are isomorphic can have different instances:
import qualified Data.Text as T
data InterspersedText = MkI {getText :: T.Text} deriving Show -- (1)!
instance Semigroup InterspersedText where --(2)!
(MkI t1) <> (MkI t2) = MkI
$ T.concat
$ fmap (\(c1, c2) -> T.pack [c1,c2])
$ T.zip t1 t2
-
InterspersedText
andText
contain the same data in the sense thatMkI :: Text -> InterspersedText
andgetText :: InterspersedText -> Text
map back and forth losslessly. -
This isn't a sensible instance in practice and is only exemplary, not least because it breaks the associativity law for
Semigroup
InterspersedText
and Text
contain the same information, but have different Semigroup
instances.
The Monoid
instances Sum and Product yield a more practical example:
-- the (Sum Int) Monoid
> Sum 4
Sum {getSum = 4}
> :t Sum 4
Sum 4 :: Num a => Sum a
> Sum 4 <> Sum 5
Sum {getSum = 9}
-- the (Product Int) Monoid
> Product 4
Product {getProduct = 4}
> :t Product 4
Product 4 :: Num a => Product a
> Product 4 <> Product 5
Product {getProduct = 20}
Constraint implication (classes)¶
One typeclass may depend on another:
What this means is that any instance of Monoid must first be an instance of Semigroup (as well as implementing the Monoid
method mempty
).
This means that if you encounter a type that is an instance of Monoid
, then it will be an instance of Semigroup
and so you can use the method <>
. For this reason, this is often called inheritance , although the relationship to inheritance in other languages is not direct.
Constraint implication (instances)¶
Read this as saying: for any type a
, if a
is an instance of Eq
, then [a]
is also an instance of Eq
.
Similarly:
This states that if a
is an instance of Num
(as are e.g. Int
and Double
) then Sum a
(or concretely, Sum Int
or Sum Int
) are instances of Monoid
.
This allows Haskell's type checker to make potentially quite complex deductions. For example:
Haskell knows that ==
can be called on x
and y
. How?
- It knows that
x
andy
both have type[a]
- It knows that
a
is an instance ofOrd
(from the type signature) - It knows that
Ord a
impliesEq a
- It knows that
Eq a
impliesEq [a]
Note
Libraries like lens use the ability of the type checker to make these deductions in sophisticated ways.
Typeclass error messages¶
> import Data.List
> data Piece = Bishop | Knight deriving Eq
> sort [Knight, Bishop]
"No instance for (Ord Piece) arising from a use of ‘sort’..."
The error is raised because sort
has type sort :: Ord a => [a] -> [a]
, which means that it expects as input, a list of values of a type which is an instance of the Ord
class.
Using typeclasses¶
Warning
It is recommended that you avoid creating your own type classes unless it is entirely necessary. This is because:
- There is usually a solution to a problem which doesn't require typeclasses.
- It is easy to create a typeclass that is badly designed.
Instead, rely on existing type classes from libraries.
Constraints float up¶
5 :: Num a => a
> :t 5
5 :: Num a => a
> :t [5, 4]
[5, 4] :: Num a => [a] -- (1)!
> :t (True, 4)
(True, 4) :: Num b => (Bool, b)
-- with custom type
> data Square a = Empty | Piece a
> :t Piece 4
Piece 4 :: Num a => Square a
- Not
[forall a . Num a => a]
which is a very different type, beyond the scope of this guide.
Similarly, if a function which requires a constraint is used as part of a larger program, that constraint floats to the top:
Because notEqual
is called on (x, x)
, x
must be of a type that is an instance of Eq
.
The compiler will reason in this way, even if you don't write down the type signature yourself.
Tip
A common difficulty that you may encounter is that you don't know what instance of a typeclass is being invoked:
-- first example
{-# LANGUAGE OverloadedStrings #-}
import Data.Text
example :: Text
example = "hello" `append` mempty
In this case, you know the type of mempty
, which is mempty :: forall a. Monoid a => a
. However, you do not know which instance of Monoid
is being used when mempty
is called. Mousing over mempty
in VSCode will reveal that the instance is Text
.
You can then look up Text
on Hackage and find the source, which gives the definition of mempty
for Text
.
Type class recursion¶
Type class instances may use the very method they are defining in the definition.
Note
Here, ==
on the right hand side of the definition is the Eq
method for Int
, but on the right hand side, it is the method for (Int, Int)
.
Type classes over others kinds¶
In the typeclass Eq
, types which are instances are normal types like Int
, Bool
, or Either Int Bool
, i.e. types with kind *
.
However, for many important typeclasses, the types which are instances have other kinds, like * -> *
. For instance:
This means that []
or Maybe
are candidates to be instances of Functor
, but Int
, Bool
, (or even [Int]
) are not.
Created: January 8, 2023