Haskell/Classes and types
Back in Type basics II we had a brief encounter with type classes as the mechanism used with number types. As we hinted back then, however, classes have many other uses.
Broadly speaking, the point of type classes is to ensure that certain operations will be available for values of chosen types. For example, if we know a type belongs to (or, to use the jargon, instantiates) the class Fractional
, then we are guaranteed, among other things, to be able to perform real division with its values.[1]
Classes and instances
editUp to now we have seen how existing type classes appear in signatures such as:
(==) :: (Eq a) => a -> a -> Bool
Now it is time to switch perspectives. First, we quote the definition of the Eq
class from Prelude:
class Eq a where
(==), (/=) :: a -> a -> Bool
-- Minimal complete definition:
-- (==) or (/=)
x /= y = not (x == y)
x == y = not (x /= y)
The definition states that if a type a
is to be made an instance of the class Eq
it must support the functions (==)
and (/=)
- the class methods - both of them having type a -> a -> Bool
. Additionally, the class provides default definitions for (==)
and (/=)
in terms of each other. As a consequence, there is no need for a type in Eq
to provide both definitions - given one of them, the other will be generated automatically.
With a class defined, we proceed to make existing types instances of it. Here is an arbitrary example of an algebraic data type made into an instance of Eq
by an instance declaration:
data Foo = Foo {x :: Integer, str :: String}
instance Eq Foo where
(Foo x1 str1) == (Foo x2 str2) = (x1 == x2) && (str1 == str2)
And now we can apply (==)
and (/=)
to Foo
values in the usual way:
*Main> Foo 3 "orange" == Foo 6 "apple" False *Main> Foo 3 "orange" /= Foo 6 "apple" True
A few important remarks:
- The class
Eq
is defined in the Standard Prelude. This code sample defines the typeFoo
and then declares it to be an instance ofEq
. The three definitions (class, data type, and instance) are completely separate and there is no rule about how they are grouped. This works both ways: you could just as easily create a new classBar
and then declare the type Integer to be an instance of it.
- Classes are not types, but categories of types, and so the instances of a class are types instead of values.[2]
- The definition of
(==)
forFoo
relies on the fact that the values of its fields (namelyInteger
andString
) are also members ofEq
. In fact, almost all types in Haskell are members of Eq (the most notable exception being functions).
- Type synonyms defined with type keyword cannot be made instances of a class.
Deriving
editSince equality tests between values are commonplace, in all likelihood most of the data types you create in any real program should be members of Eq. A lot of them will also be members of other Prelude classes such as Ord and Show. To avoid large amounts of boilerplate for every new type, Haskell has a convenient way to declare the "obvious" instance definitions using the keyword deriving. So, Foo would be written as:
data Foo = Foo {x :: Integer, str :: String}
deriving (Eq, Ord, Show)
This makes Foo an instance of Eq with an automatically generated definition of == exactly equivalent to the one we just wrote, and also makes it an instance of Ord and Show for good measure.
You can only use deriving with a limited set of built-in classes, which are described very briefly below:
- Eq
- Equality operators == and /=
- Ord
- Comparison operators < <= > >=; min, max, and compare.
- Enum
- For enumerations only. Allows the use of list syntax such as [Blue .. Green].
- Bounded
- Also for enumerations, but can also be used on types that have only one constructor. Provides minBound and maxBound as the lowest and highest values that the type can take.
- Show
- Defines the function show, which converts a value into a string, and other related functions.
- Read
- Defines the function read, which parses a string into a value of the type, and other related functions.
The precise rules for deriving the relevant functions are given in the language report. However, they can generally be relied upon to be the "right thing" for most cases. The types of elements inside the data type must also be instances of the class you are deriving.
This provision of special "magic" function synthesis for a limited set of predefined classes goes against the general Haskell philosophy that "built in things are not special", but it does save a lot of typing. Besides that, deriving instances stops us from writing them in the wrong way (an example: an instance of Eq
such that x == y
would not be equal to y == x
would be flat out wrong). [3]
Class inheritance
editClasses can inherit from other classes. For example, here is the main part of the definition of Ord in Prelude:
class (Eq a) => Ord a where
compare :: a -> a -> Ordering
(<), (<=), (>=), (>) :: a -> a -> Bool
max, min :: a -> a -> a
The actual definition is rather longer and includes default implementations for most of the functions. The point here is that Ord inherits from Eq. This is indicated by the => notation in the first line, which mirrors the way classes appear in type signatures. Here, it means that for a type to be an instance of Ord it must also be an instance of Eq, and hence needs to implement the == and /= operations.[4]
A class can inherit from several other classes: just put all of its superclasses in the parentheses before the =>. Let us illustrate that with yet another Prelude quote:
class (Num a, Ord a) => Real a where
-- | the rational equivalent of its real argument with full precision
toRational :: a -> Rational
Standard classes
editThis diagram, adapted from the Haskell Report, shows the relationships between the classes and types in the Standard Prelude. The names in bold are the classes, while the non-bold text stands for the types that are instances of each class ((->) refers to functions and [], to lists). The arrows linking classes indicate the inheritance relationships, pointing to the inheriting class.
Type constraints
editWith all pieces in place, we can go full circle by returning to the very first example involving classes in this book:
(+) :: (Num a) => a -> a -> a
(Num a) =>
is a type constraint, which restricts the type a
to instances of the class Num
. In fact, (+)
is a method of Num
, along with quite a few other functions (notably, (*)
and (-)
; but not (/)
).
You can put several limits into a type signature like this:
foo :: (Num a, Show a, Show b) => a -> a -> b -> String
foo x y t =
show x ++ " plus " ++ show y ++ " is " ++ show (x+y) ++ ". " ++ show t
Here, the arguments x and y must be of the same type, and that type must be an instance of both Num and Show. Furthermore, the final argument t must be of some (possibly different) type that is also an instance of Show. This example also displays clearly how constraints propagate from the functions used in a definition (in this case, (+)
and show
) to the function being defined.
Other uses
editBeyond simple type signatures, type constraints can be introduced in a number of other places:
instance
declarations (typical with parametrized types);
class
declarations (constraints can be introduced in the method signatures in the usual way for any type variable other than the one defining the class[5]);
data
declarations,[6] where they act as constraints for the constructor signatures.
Note
Type constraints in data
declarations are less useful than it might seem at first. Consider:
data (Num a) => Foo a = F1 a | F2 a String
Here, Foo is a type with two constructors, both taking an argument of a type a
which must be in Num
. However, the (Num a) =>
constraint is only effective for the F1 and F2 constructors, and not for other functions involving Foo
. Therefore, in the following example...
fooSquared :: (Num a) => Foo a -> Foo a
fooSquared (F1 x) = F1 (x * x)
fooSquared (F2 x s) = F2 (x * x) s
... even though the constructors ensure a
will be some type in Num
we can't avoid duplicating the constraint in the signature of fooSquared
.[7]
A concerted example
editTo provide a better view of the interplay between types, classes, and constraints, we will present a very simple and somewhat contrived example. We will define a Located
class, a Movable
class which inherits from it, and a function with a Movable
constraint implemented using the methods of the parent class, i.e. Located
.
-- Location, in two dimensions.
class Located a where
getLocation :: a -> (Int, Int)
class (Located a) => Movable a where
setLocation :: (Int, Int) -> a -> a
-- An example type, with accompanying instances.
data NamedPoint = NamedPoint
{ pointName :: String
, pointX :: Int
, pointY :: Int
} deriving (Show)
instance Located NamedPoint where
getLocation p = (pointX p, pointY p)
instance Movable NamedPoint where
setLocation (x, y) p = p { pointX = x, pointY = y }
-- Moves a value of a Movable type by the specified displacement.
-- This works for any movable, including NamedPoint.
move :: (Movable a) => (Int, Int) -> a -> a
move (dx, dy) p = setLocation (x + dx, y + dy) p
where
(x, y) = getLocation p
A word of advice
editDo not read too much into the Movable
example just above; it is merely a demonstration of class-related language features. It would be a mistake to think that every single functionality which might be conceivably generalized, such as setLocation
, needs a type class of its own. In particular, if all your Located
instances should be able to be moved as well then Movable
is unnecessary - and if there is just one instance there is no need for type classes at all! Classes are best used when there are several types instantiating it (or if you expect others to write additional instances) and you do not want users to know or care about the differences between the types. An extreme example would be Show
: general-purpose functionality implemented by an immense number of types, about which you do not need to know a thing before calling show
. In the following chapters, we will explore a number of important type classes in the libraries; they provide good examples of the sort of functionality which fits comfortably into a class.
Notes
- ↑ To programmers coming from object-oriented languages: A class in Haskell in all likelihood is not what you expect - don't let the terms confuse you. While some of the uses of type classes resemble what is done with abstract classes or Java interfaces, there are fundamental differences which will become clear as we advance.
- ↑ This is a key difference from most OO languages, where a class is also itself a type.
- ↑ There are ways to make the magic apply to other classes. GHC extensions allow
deriving
for a few other common classes for which there is only one correct way of writing the instances, and the GHC generics machinery make it possible to generate instances automatically for custom classes. - ↑ If you check the full definition in the Prelude specification, the reason for that becomes clear: the default implementations involve applying
(==)
to the values being compared. - ↑ Constraints for the type defining the class should be set via class inheritance.
- ↑ And
newtype
declarations as well, but nottype
. - ↑ Extra note for the curious: This issue is related to some of the problems tackled by the advanced features discussed in the "Fun with types" chapter of the Advanced Track.