5

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?

3
  • It seems that one of my points is incorrect: I was certain I had managed to do the whole "Eq on Set, Ord on BinarySearchTree" thing but I can't get that to compile anymore.... Commented Aug 21, 2014 at 9:19
  • Note: this question is related to another one: stackoverflow.com/questions/25345055/…. Not sure if it's close enough to be considered a duplicate... Commented Aug 21, 2014 at 11:47
  • I'm not sure they qualify as duplicates, even though they do both seem to come from the same root problem. Either way is fine by me, and I would not be offended for my question to be closed as a duplicate. Commented Aug 21, 2014 at 12:04

2 Answers 2

11

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.

Sign up to request clarification or add additional context in comments.

3 Comments

In my very specific case, it seems I can solve the problem with ExistentialQuantification by not setting any constraint on Set and adding the Ord a constraint to BinarySearchTree's data constructors. Is there any particular reason not to do that?
I'm not sure I understand what you mean. Do you keep the Set class as shown in your original question? If so, what prevents me from calling insert (insert empty x) y for x and y of a type a without an Ord a instance?
Sorry, that wasn't very clear. I've answered my own question to provide more details and would appreciate any insight you might have - I'm enough of a Haskell beginner that odds are good I'm not seeing all the consequences of the implementation I propose.
0

I've got something that seems to work.

It requires redefining Set as follows:

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

Note the differences with the earlier definition:

  • there is no class constraint.
  • there is no empty method.

Having defined Set that way, I can then define BinarySearchTree as:

{-# LANGUAGE ExistentialQuantification #-}
data BinarySearchTree a = Ord a => Node a (BinarySearchTree a) (BinarySearchTree a)
                        | Ord a => Leaf

This works, because BinarySearchTree cannot be constructed without an implicit Ord anymore - whenever you get an instance BinarySearchTree a, you also have Ord a.

Note that this required changing the Set signature not to include an empty definition anymore: since it does not receive an instance of BinarySearchTree a but must create one, there is no way to enforce the Ord a constraint.

In this specific instance, I don't believe this to be much of an issue: unless I'm mistaken, in order to use the empty method, you'd need to explicitly type its return value. As long as you're going to do that, you might as well use the empty constructor for the underlying implementation.

1 Comment

Sure, if you don't need an overloaded empty, then this works. It does mean that any function that does need empty will necessarily be restricted to a single Set implementation. Depending on your use case, this may not be a problem, but it does limit the generality of your solution.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.