Multi-parameter Type Classes in Haskell

2015-03-14

Multi-parameter type classes are used when writing a class that needs to be parameterized over two (or more) different types. When would you want to do this? Let's look at a simple example adapted from the Haskell Wikibook.

class Collection c where
    insert :: c -> e -> c

-- Make lists an instance of Collection:
instance Collection [a] where
    insert xs x = x:xs -- this doesn't work

As the Haskell Wikibook tells us, this will not compile. The e comes from nowhere. Its type is not constrained so it can be anything. The only thing we could possibly do is pass it to id. In the Collection instance, we can't cons x and xs because we do not have any assurance that xs are a list of the x type.

What we really need is a way to show that there is some relation between c and e (and therefore a relation between x and xs). This is where multi-parameter type classes come in.

Multi-Parameter Type Classes

The following code is a correct version of the above.

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
class Collection c e where
    insert :: c -> e -> c

-- Make lists an instance of Collection:
instance Collection [a] a where
    insert xs x = x:xs

This code compiles. In the Collection instance, we make sure that the c type becomes a list of a, and the e type becomes an a.

There is, however, one small problem with this code. A given c should always determine the type of e. If c is [a], then e will always be a. It will never be something else like String, Int, b, etc. How can we enforce this we we are writing our type class?

There are basically two solutions, functional dependencies and type families.

I'll give one more example to motivate this. Imagine an Add type class that represents two types that are able to be added together.

{-# LANGUAGE MultiParamTypeClasses #-}
class Add a b c where
    plus :: a -> b -> c

instance Add Integer Integer Integer where
    plus x y = x + y

-- we don't actually want to allow this
instance Add Integer Integer Double where
    plus x y = fromIntegral x + fromIntegral y

It's easy to see that the type of c will be determined based on the type of a and b. We don't actually want to allow both Add Integer Integer Integer and Add Integer Integer Double. In order to get the type system to throw an error when the user does this, we need to use either functional dependencies or type families.

Functional Dependencies

Functional Dependencies are the simpler of the two solutions.

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}
class Add a b c | a b -> c where
    plus :: a -> b -> c

instance Add Integer Integer Integer where
    plus x y = x + y

-- The following instance declaration throws an error!
-- Just like we wanted.
instance Add Integer Integer Double where
    plus x y = fromIntegral x + fromIntegral y

-- The following instance works because a and b are unique.
instance Add Integer Double Double where
    plus x y = fromIntegral x + y

In the type class definition we use a vertical bar followed by the constraints we want to impose. In a b -> c, we are saying that a and b are able to uniquely determine c.

Type Families

Type families are a much more powerful solution. It is possible to do many things with type families. I will only introduce a small use-case which will solve our problem above. The following is a solution using type families.

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
class Add a b where
    type SumType a b
    plus :: a -> b -> SumType a b

instance Add Integer Integer where
    type SumType Integer Integer = Integer
    plus x y = x + y

-- The following instance declaration throws an error.
instance Add Integer Integer where
    type SumType Integer Integer = Double
    plus x y = fromIntegral x + fromIntegral y

In the class definition, we are creating a type alias SumType a b and using it in type of plus. In the instance declaration, we are assigning SumType a b to be equal to Integer, so in the class definition wherever you see SumType a b, you can replace it with Integer. Therefore, in the instance definition, the type of plus becomes Integer -> Integer -> Integer.

Conclusion

Let's go back and look at our original example. How can we write it with functional dependencies and type families?

The following is using functional dependencies.

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
class Collection c e | c -> e where
    insert :: c -> e -> c

instance Collection [a] a where
    insert xs x = x:xs

The following is using type families.

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
class Collection c where
    type CollectedType c
    insert :: c -> CollectedType c -> c

instance Collection [a] where
    type CollectedType [a] = a
    insert xs x = x:xs

Multi-parameter type classes, functional dependencies, and type families are all very powerful. When programming in Haskell, it is good to know when to use them. Lots of useful documentation can be found with a quick google search.

tags: haskell