Abstracting type-classes

There are several ways in which type-classes can be generalized further. In this section, we will focus on extending the number of type parameters from one to many. The extension to multiparameter type-classes demands that we specify relations between type parameters by way of functional dependencies.

Multiparameter type-classes

We can view regular type-classes (for example Ord a, Monad a, and so on.) as a way to specify a set of types. Multiparameter classes, on the other hand, specify a set of type relations. For example, the Coerce type-class specifies a relation between two type parameters:

class Coerce a b where
  coerce :: a -> b

instance Coerce Int String where
  coerce = show

instance Coerce Int [Int] where
  coerce x = [x]

The type signature of coerce is:

coerce :: Coerce a b => a -> b

This states that coerce is the function a -> b if a is coerce-able to b, that is, if the relation (Coerce a b) exists. In our case, coerce will work for (Int -> String) and (Int -> [Int]).

However, with multiple type parameters, type inference suffers; for example, the compiler rejects.

  coerce 12 :: String

We have to help it along with type annotations:

  coerce (12::Int) :: String
  coerce (12::Int) :: [Int]

This sort of type ambiguity quickly gets out of hand and was the reason for multiparameter type-classes not making it into Haskell 98, despite being part of the GHC since 1997.

Functional dependencies

It was only with the discovery of functional dependencies that multiparameter classes became more practically useful. (Mark Jones, 2000, Type Classes with Functional Dependencies: http://www.cs.tufts.edu/~nr/cs257/archive/mark-jones/fundeps.ps).

Functional dependencies give us a way to resolve the ambiguity created by multiple type parameters. For example, we can constrain the relationship between a and b in Coerce with a functional dependency:

{-# LANGUAGE FunctionalDependencies #-}

class Coerce2 a b | b -> a where
  coerce2 :: a -> b

instance Coerce2 Int String where
  coerce2 = show

instance Coerce2 Int [Int] where
  coerce2 x = [x]

The relation (b -> a) tells the compiler that if it can infer b, it can simply look up the corresponding a in one of the type-class instances. For example, given the following expression:

  coerce2 12 :: String

The compiler can infer b :: String and can find the (uniquely) corresponding type a :: Int from the instance declaration for Coerce2 Int String:

   instance Coerce2 Int String where ...

Moreover, the compiler can now prevent us from adding conflicting instance declarations, for example:

–- INVALID
instance Coerce2 Float String where
   coerce2 = show

The above instance declaration is invalid because String has already been associated to Int and cannot now also be associated with Float. The functional dependency b -> a tells the compiler that b determines a uniquely.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset