Not logged in. Login

Haskell Type Classes

Recall that Haskell's type system is polymorphic, so that we can define useful abstractions can be defined to apply in many situations.

For example, the map functional can be used to process a list of elements of one type (say a) to produce a corresponding list of elements of another type (say b). The polymorphic type signature of map reflects this in a fully general form.

map :: (a -> b) -> [a] -> [b]

Here the types a and b can stand for any types whatsoever.

Constrained Polymorphism

Sometimes, however, we need to impose constraints on the types that are used for a polymorphic abstraction.

A common constraint is that a type support the equality (==) operator. For example, if we need a function to check whether a particular value is found as an element of a list, we might produce the following definition.

isElem x [] = False
isElem x (a:as)
   | x == a      = True
   | otherwise   = isElem x as

Here, this function could work for lists of any types of element, so long as the == operator is defined.

Type Constraints

If we load this implementation into Haskell, we see that Haskell reflects the requirement for the == operator with a type constraint.

Prelude> :load isElem.hs
[1 of 1] Compiling Main             ( isElem.hs, interpreted )
Ok, modules loaded: Main.
*Main> :t isElem
isElem :: Eq a => a -> [a] -> Bool
*Main> 

Here, the notation Eq a => means that the type a must be an instance of the type class Eq, the class of types supporting the == operator.

The Eq Class

The Haskell standard library includes a definition of the type class Eq.

class  Eq a  where
   (==), (/=) :: a -> a -> Bool
       -- Minimal complete definition:
       --      (==) or (/=)
   x /= y     =  not (x == y)
   x == y     =  not (x /= y)

In essence, this states two properties of the Eq class.

  • it is the class of types that support the two operators == and /=, and
  • if one of the operators is defined, then the other is automatically available as well.

In fact, many standard types are automatically defined to be members of the Eq class, so we can use our isElem function in many contexts without further ado.

*Main> isElem 'a' "back"
True
*Main> isElem [1] [[1], [2]]
True

Can you think of any types that are not members of the == class?

Instance Declarations

To create an instance of the Eq class, we can use an instance declaration. For example, suppose we want a rational number data type to be the ratio of two integers, and to assert that two ratios are equal according to the usual rules of arithmetic. The following definition suffices.

data RationalNum = Ratio Int Int 

instance Eq RationalNum where
  (Ratio a b) == (Ratio c d)   =  a * d == b * c

Given this definition, we can now automatically use our isElem function on RationalNums.

*Main> isElem (Ratio 3 4) [Ratio 1 1, Ratio 6 8]
True

Another way that we can define instances of the Eq class is through a deriving declaration. For example, consider the following NumPair algebraic data type.

data NumPair = Pair Int Int deriving Eq

This declaration tells Haskell to automatically define the == function for this data type, based on structural equality of the components of the type.

*Main> isElem (Pair 3 4) [Pair 1 1, Pair 6 8]
False
*Main> isElem (Pair 3 4) [Pair 1 1, Pair 3 4]
True

Multiple Constraints

You can use multiple type constraints in a single definition. Here is an example from the Classes and Types section of the Haskell Wikibook.

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

The Num Class

The Num class is another important example: the class of all numeric types. It allows polymorphic functions such as (+) to be defined for all numeric types.

(+) :: (Num a) => a -> a -> a

Here the type context (Num a) => states that a is an instance of type clase Num, i.e., a numeric type.

Three important instances of type class Num are

  1. Int - fixed precision integers based on machine register size (typically 32-bit or 64-bit).
  2. Integer - arbitrary precision integers.
  3. Float - single-precision floating point numbers.

Numeric literals such as 17 and 5.21 are also polymorphic.

Prelude> :type 17
17 :: Num a => a
Prelude> :type 5.21
5.21 :: Fractional a => a

Fractional is a subclass of Num that excludes Int and Integer. Depending on context, type inference narrows types to ones which support given operations.

Prelude> :type 17 + 5.21
17 + 5.21 :: Fractional a => a
Prelude> 17 + 5.21
22.21

In the example, 17 is first narrowed from Num to Fractional to match the polymorphic Fractional type.

The Ord Class

The Ord Class is for types that have an ordering that supports the (<), (<=), (>=) and (>) functions.

Many standard types are members of the Ord class automatically.

Prelude> [3] < [4]
True

What does it mean for one list to be less than another?

Instance declarations for Ord only need to define one of the comparison functions.

instance Ord RationalNum where
  (Ratio a b) <= (Ratio c d)   =  a * d <= b * c

The other functions are automatically defined.

*Main> Ratio 3 4 < Ratio 1 1
True
*Main> Ratio 3 4 < Ratio 6 8
False

Instances can also be generated automatically with the deriving keyword.

data NumPair = Pair Int Int deriving (Eq, Ord)

The comparison functions are generated automatically.

*Main> Pair 1 2 < Pair 1 3
True
*Main> Pair 3 4 < Pair 6 8
True
*Main> Pair 3 4 < Pair 3 1
False

Type Classes vs. OOP Classes

Although there are some similarities, Haskell type classes are quite different from classes in OOP. Do not confuse them.

Updated Sat Sept. 01 2018, 18:24 by cameron.