CSci 555: Functional Programming
Spring Semester 2007
Lecture Notes


Overloading and Type Classes

Polymorphism in Haskell

(Pure) polymorphism:
A single function definition is used for all types of arguments and results.
For example, the length function.

Overloading:
The same name refers to different functions depending upon the type.
For example, the (+) function.

These notes concern overloading.

Why Overloading?

Consider testing for membership of a Boolean list, where eqBool is an equality-testing function for booleans.

    elemBool :: Bool -> [Bool] -> Bool
    elemBool x []     = False
    elemBool x (y:ys) = eqBool x  y || elemBool x ys

The above is not very general. It works for booleans, but what if we want to handle lists of integers? or of characters?

Thus let's consider testing for membership of a general list, with the equality function as a parameter.

    elemGen :: (a -> a -> Bool) -> a -> [a] -> Bool
    elemGen eqFun x []      = False
    elemGen eqFun x (y:ys)  = eqFun x y || elemGen eqFun x ys

    elemBool :: Bool -> [Bool] -> Bool
    elemBool = elemGen (eqBool)

But really the function elemGen is too general for the intended function. Parameter eqFun could be any

    a -> a -> Bool
function, not just equality.

Another problem is that equality is a meaningless idea for some data types. For example, comparing functions for equality is a computationally intractable problem.

The alternative to the above to make (==) (i.e., equality) an overloaded function. We can then restrict the polymorphism in elem's type signature to those types for which (==) is defined.

We introduce the concept of type classes to to be able to define the group of types for which an overloaded operator can apply.

We can then restrict the polymorphism of a type signature to a class by using a context constraint as "Eq a =>" is used below:

    elem :: Eq a => a -> [a] -> Bool

Defining an Equality Class and Its Instances

We can define class Eq to be the set of types for which we define the (==) (i.e., equality) operation.

For example, we might define the class as follows, giving the type signature(s) of the associated function(s) (also called the operations or methods of the class).

    class Eq a where
        (==) :: a -> a -> Bool

A type is made a member or instance of a class by defining the signature function(s) for the type. For example, we might define Bool as instance of Eq as follows:

    instance Eq Bool where
        True  == True  = True
        False == False = True
        _     == _     = False

Other types, such as the primitive types Int and Char, can also be defined as instances of the class. Comparison of primitive data types will often be implemented as primitive operations built into the computer hardware, as follows:

    instance Eq  Int where 
        (==) = primEqInt
    instance Eq Char where 
        (==) = primEqChar

An instance declaration can also be declared with a context constraint, such as in the equality of lists:

    instance Eq a => Eq [a] where
        []     == []     =  True
        (x:xs) == (y:ys) =  x==y && xs==ys
         _     == _      =  False

Within the class Eq, the (==) function is overloaded. The definition of (==) given for the types of its actual operands is used in evaluation.

In the Haskell standard prelude, the class definition for Eq includes both the equality and inequality functions. They may also have default definitions as follows:

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

In the case of Eq, inequality is defined as the negation of equality and vice versa.

An instance declaration must override (i.e., redefine) at least one of these functions (in order to break the circular definition), but the other function may either be left with its default definition or overridden.

Another Example Class

Another example class is Visible, which might denote types whose values can be displayed as strings.

    class Visible a where
        toString :: a -> String
        size     :: a -> Int

Various data types can be made instances of this class:

    instance Visible Char where
        toString ch  = [ch]
        size _       = 1

    instance Visible Bool where
        toString True  = "True"
        toString False = "False"
        size _         = 1

    instance Visible a => Visible [a] where
        toString = concat . map toString  
        size     = foldr (+) 1 . map size  

Class Extension (Inheritance)

Haskell supports the concept of class extension. That is, a new class can be defined that inherits all the operations of another class and adds additional operations.

For example, an ordering class Ord can be derived from the class Eq, perhaps as follows. (The definition in the standard prelude differs from the following.)

    class Eq a => Ord a where
        (<), (<=), (>), (>=) :: a -> a -> Bool
        max, min             :: a -> a -> a
        -- Minimal complete definition: (<) or (>)
        x <= y                  = x < y || x == y
        x <  y                  = y > x
        x >= y                  = x > y || x == y
        x >  y                  = y < x
        max x y   | x >= y      = x
	          | otherwise   = y
        min x y   | x <= y      = x
	          | otherwise   = y

Using the above definition, Ord is a subclass of Eq. Eq is a superclass of Ord.

A function such as isort (insertion sort) uses class Ord to defining sorting of ordered data items, as follows:

        iSort :: Ord a => [a] -> [a]
        iSort []     = []
        iSort (x:xs) = ins x (iSort xs)

        ins :: Ord a => a -> [a] -> [a]
        ins x []        = [x]
        ins x (y:ys)
            | x <= y	= x:y:ys
            | otherwise	= y : ins x ys

Multiple Constraints

Haskell also permits classes to be constrained by two or more other classes.

Consider the problem of sorting a list and then displaying the results as a string:

    vSort :: (Ord a,Visible a) => [a] -> String
    vSort = toString . iSort 

To sort the elements, they need to be from an ordered type. To convert the results to a string, we need them to be from a Visible type.

The multiple contraints can be over two different parts of the signature of a function. Consider a program that displays the second components of tuples if the first component is equal to a given value:

    vLookupFirst :: (Eq a,Visible b) => [(a,b)] -> a -> String
    vLookupFirst xs x = toString (lookupFirst xs x)

    lookupFirst :: Eq a => [ (a,b) ] -> a -> [b]
    lookupFirst ws x  = [ z | (y,z) <- ws , y==x ]

Multiple constraints can occur in an instance declaration, such as might be used in extending equality to cover pairs:

 
    instance (Eq a,Eq b) => Eq (a,b) where
        (x,y) == (z,w) =  x==z && y==w

Multiple constraints can also occur in the definition of a class, as might be the case in definition of an ordered visible class.

    class (Ord a,Visible a) => OrdVis a
    
    vSort :: OrdVis a => [a] -> String

The case where a class extends two or more classes, as above for OrdVis is called multiple inheritance.

Built-In Haskell Classes

See section 12.4 of the Thompson textbook for discussion of various classes in Haskell's standard prelude.

Comparison to Java and C++


Acknowledgments

These notes are based on:


UP to CSCI 555 Lecture Notes root document?


Prepared by H. Conrad Cunningham
Last modified: 1 March 2007