functions not in type classes

Rijk-Jan van Haaften rjchaaft@cs.uu.nl
Fri, 18 Jan 2002 15:01:23 +0100


>Thanks. I am quite convinced actually (no sarchasm involved).

I'm sorry I didn't follow the discussion. From the last few mails,
it seems that the difference between fully polymorphic functions like
map, id, filter etc. on one hand and overloaded functions on the other
hand is not clear:

> > > > the goal of type class is to allow overloading of function, so for
> > > > example
> > > > id x = x     is not subject to overloading => no use to put it in a
> > > > type class.
> > >
> > > Because it doesn't do anything?
> >
<examples by Dave showing that id is useful>

There are fairly simple rules to decide whether or not a function should
be in a type class.

Notes:
1. The type of x is NOT needed/used <=> there is NO           pattern match 
on x
2. The type of x is     needed/used <=> there is AT LEAST ONE pattern match 
on x
3. Recursive checks are necessary sometimes. This will become clear in the 
filter example

A. In a type class
Rule:
Put a function f in a type class if and only if
For some argument x:
   (x ranges over more than one type)
   and
   (the function needs the type of x in order to do its work)

B. Not in a type class
Rule:
Do NOT put a function f in a type class if (and only if)
For each argument x of f:
   (f doesn't use the type of x)
   or
   (x doesn't range over multiple types (ie has a single type))

If someone has a better definition, I would be glad to hear it.

I will explain the rules using a few examples.

id :: a -> a
id x = x

There is just one argument to check: x
Property: there is no pattern match on x
We conclude that rule B applies
The check that rule A doesn't apply is left as an exercise to the reader.

filter :: (a -> Bool) -> [a] -> [a]
filter p []                 = []
filter p (x:xs) | p x       = x : filter p xs
                 | otherwise = filter p xs

there are two arguments: the function p and the list
p is just passed around and it can take any type satisfying
(a -> Bool) like (Int -> Bool), (Char -> Bool) etc.
Now note 3 (above) comes up.
The list is a difficult case. Does it range over multiple
types or doesn't it? filter takes as second argument a list,
and only a list. No other structure is allowed. Therefore,
it doesn't take a range of types there. It takes a single type.
But because it is unpacked using pattern matching, we have to
check the properties of its components x and xs. The type of x
is not used. The list xs is pattern-matched (implicit in
"filter p xs") but again, the list itself is only a single
structure.

The function map is left as an exercise to the reader.

Quite regularly we have to work with datatypes other than lists.
For example binary trees with elements in the nodes:

data BinTree a = Node a (BinTree a) (BinTree a) | Leaf

To convert a (BinTree a) to a (BinTree b) by applying a
function f :: a -> b on each element, we need a function very
similar to map. Unfortunately, map only accepts lists, so
we have to write our own function. Of course the technical
behaviour is quite different, but for the programmer the
behaviour is very similar.
For this situation we have
fmap :: (a -> b) -> f a -> f b

For the type f (of kind * -> *) we can take []
(note that "x :: [] a" is the same as "x :: [a]")
or BinTree or some other collection type. However,
not an arbitrary type, since the technical implementation
of fmap for [] differs from the one for BinTree.
Therefore, we have no uniform implementation working
for an arbitrary type f.
Note the requirement in A
   (the function needs the type of x in order to do its work)
In this case, we need the type of f to choose the right
implementation.
Thus, we have to put in in a class:
class Functor f where
   fmap :: (a -> b) -> f a -> f b

For Maybe (a collection type with only two choices:
no elements or one element) we can write:
instance Functor Maybe where
   fmap f Nothing  = Nothing
   fmap f (Just x) = Just (f x)

For lists we have (the usual map)
instance Functor [] where
   fmap f []     = []
   fmap f (x:xs) = f x : fmap f xs

For BinTree we have
instance Functor BinTree where
   fmap f Leaf           = Leaf
   fmap f (Node x t1 t2) = Node (f x) (fmap f t1) (fmap f t2)

If we try "fmap (+3) 5", the compiler will tell us that
5 is and Int, and fmap needs Maybe a, [a] or BinTree a.
In the three implementations, you see that the second
argument of fmap ranges over three different types, while
it needs the type of that argument to perform the map.

I hope this clears up a few difficulties.

Rijk-Jan