Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Arbitrary class constraints when implementing type classes in Haskell

Tags:

haskell

I'm trying to implement a simple Set in Haskell, and am getting stuck with how to express class constraints for the elements it contains.

The Set type class is fairly simple:

class Set s where
  empty    :: s a
  isEmpty  :: s a -> Bool
  insert   :: s a -> a -> s a
  contains :: s a -> a -> Bool

A trivial implementation of a Set is a binary search tree:

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

I'm getting somewhat stuck when it comes to declaring the correct class constraints, however:

  • Set requires, at the very least, the ability to decide whether two elements are equal - its values need to have an instance of Eq.
  • BinarySearchTree requires its elements to have an instance of Ord, since each node needs to have the smaller element on the left and the bigger on the right.

The first, simple solution is to update Set's signature to require a to have an instance of Eq:

class Set s where
  empty    :: Eq a => s a
  isEmpty  :: Eq a => s a -> Bool
  insert   :: Eq a => s a -> a -> s a
  contains :: Eq a => s a -> a -> Bool

Implementing an instance of Set for BinarySearchTree is not a problem: Ord implies Eq, so I can "override' the class constraints.

What if, however, BinarySearchTree required some exotic class contraints, ones that are entirely incompatible with Eq? How do I express these and keep the type checker happy?

The only way I can think of is to add a class constraint on BinarySearchTree, something like:

data Ord a => BinarySearchTree a = Node a (BinarySearchTree a) (BinarySearchTree a) | Leaf

Unfortunately, this does not seem to satisfy the type checker: even though the declaration guarantees that BinarySearchTree must contain elements with an Ord instance, this constraint doesn't seem to carry over to functions that use BinarySearchTree - it's as if the class constraint only applied to the data constructors, but were then forgotten about.

What am I missing? Is there an elegant solution for what I'm trying to do, or even a solution at all?

like image 642
Nicolas Rinaudo Avatar asked Aug 21 '14 09:08

Nicolas Rinaudo


People also ask

What are class constraints in Haskell?

Everything before the => symbol is called a class constraint. We can read the previous type declaration like this: the equality function takes any two values that are of the same type and returns a Bool. The type of those two values must be a member of the Eq class (this was the class constraint).

How does deriving work in Haskell?

The second line, deriving (Eq, Show) , is called the deriving clause; it specifies that we want the compiler to automatically generate instances of the Eq and Show classes for our Pair type. The Haskell Report defines a handful of classes for which instances can be automatically generated.

What is it called when the type of a function contains one or more class constraints Haskell?

Overloaded Functions. A polymorphic function is called overloaded if its type contains one or more class constraints. (+) :: Num a ⇒ a -> a -> a.

What do you mean by type class in Haskell?

What's a typeclass in Haskell? A typeclass defines a set of methods that is shared across multiple types. For a type to belong to a typeclass, it needs to implement the methods of that typeclass. These implementations are ad-hoc: methods can have different implementations for different types.


1 Answers

You're asking about a well-known problem in Haskell: how to define a type class such that instances of the type class can define the type class constraints they require for the type class's operations. This problem often appears on discussion forums in the form of the question "Why is Data.Set not an instance of Functor?" and then the problem is that Data.Set has a map function with an additional Ord constraint:

Data.Set.map :: Ord b => (a -> b) -> Set a -> Set b 

while Functor's fmap method looks like

class Functor f where
  fmap :: (a -> b) -> f a -> f b

Since a few years, solutions to the problem exist. One solution combines the relatively new extension ConstraintKinds with TypeFamilies. For your example, it would amount to something like this:

class Set s where
  type SetCt s a :: Constraint
  empty    :: s a
  isEmpty  :: s a -> Bool
  insert   :: Ct a => s a -> a -> s a
  contains :: Ct a => s a -> a -> Bool

The instance for BinarySearchTree would then look like

instance Set BinarySearchTree where
  type SetCt BinarySearchTree a = Ord a
  ...

Here is a blog post by Dominic Orchard which explains these ideas in a bit more detail.

like image 123
Dominique Devriese Avatar answered Sep 28 '22 10:09

Dominique Devriese