views:

386

answers:

4

Suppose I have the following:

class Shape a where
    draw a :: a -> IO ()

data Rectangle = Rectangle Int Int

instance Shape Rectangle where
    draw (Rectangle length width) = ...

data Circle = Circle Int Int

instance Shape Circle where
    draw (Circle center radius) = ...

Is there any way for me to define a list of shapes, traverse over the list, and call the draw function on each shape? The following code won't compile because the list elements aren't all the same type:

shapes = [(Circle 5 10), (Circle 20, 30), (Rectangle 10 15)]

I know I'm thinking in an OO way and trying to apply it to Haskell, and that might not be the best approach. What would be the best Haskell approach for programs that need to deal with collections of different types of objects?

+8  A: 

Consider using a single type instead of separate types and a typeclass.

data Shape = Rectangle Int Int
           | Circle Int Int

draw (Rectangle length width) = ...
draw (Circle center radius)   = ...

shapes = [Circle 5 10, Circle 20 30, Rectangle 10 15]
Dave Hinton
While this works, I was looking for a solution that allows new data types to be defined elsewhere so that the code that processes the shapes list doesn't need to be aware of every type of shape.
Clint Miller
+10  A: 

If you really do need to do this, then use an existential:

{-# LANGUAGE GADTs #-}


class IsShape a where
    draw :: a -> IO ()

data Rectangle = Rectangle Int Int

instance IsShape Rectangle where
    draw (Rectangle length width) = ...

data Circle = Circle Int Int

instance IsShape Circle where
    draw (Circle center radius) = ...

data Shape where
    Shape :: IsShape a => a -> Shape

shapes = [Shape (Circle 5 10), Shape (Circle 20 30), Shape (Rectangle 10 15)]

(I renamed your class as there would be a name clash with the datatype otherwise, and having the naming this way round seems more natural).

The advantage of this solution over the other answer involving a single datatype with different constructors is that it is open; you can define new instances of IsShape wherever you like. The advantage of the other answer is that it's more "functional", and also that the closedness may in some cases be an advantage as it means that clients know exactly what to expect.

Ganesh Sittampalam
I couldn't get your example to compile. But, the wiki page you reference answers my question perfectly.
Clint Miller
Try now - I had a `Shape` where I should have had an `IsShape` in the signature of the data constructor for `Shape`.
Ganesh Sittampalam
+3  A: 

As Ganesh said, you could indeed use GADTs to have more type safety. But if you don't want (or need) to, here's my take on this:

As you already know, all elements of a list need to be of the same type. It isn't very useful to have a list of elements of different types, because then your throwing away your type information.

In this case however, since you want throw away type information (you're just interested in the drawable part of the value), you would suggest to change the type of your values to something that is just drawable.

type Drawable = IO ()

shapes :: [Drawable]
shapes = [draw (Circle 5 10), draw (Circle 20 30), draw (Rectangle 10 15)]

Presumably, your actual Drawable will be something more interesting than just IO () (maybe something like: MaxWidth -> IO ()).

And also, due to lazy evaluation, the actual value won't be drawn until you force the list with something like sequence_. So you don't have to worry about side effects (but you probably already saw that from the type of shapes).


Just to be complete (and incorporate my comment into this answer): This is a more general implementation, useful if Shape has more functions:

type MaxWith = Int

class Shape a where
    draw :: a -> MaxWidth -> IO ()
    size :: a -> Int

type ShapeResult = (MaxWidth -> IO (), Int)

shape :: (Shape a) => a -> ShapeResult
shape x = (draw x, size x)

shapes :: [ShapeResult]
shapes = [shape (Circle 5 10), shape (Circle 20 30), shape (Rectangle 10 15)]

Here, the shape function transforms a Shape a value into a ShapeResult value, by simply calling all the functions in the Shape class. Due to laziness, none of the values are actually computed until you need them.

To be honest, I don't think I would actually use a construct like this. I would either use the Drawable-method from above, or if a more general solution is needed, use GADTs. That being said, this is a fun exercise.

Tom Lokhorst
This is nice if drawing is really all you want to do with them - if you might want to do different things from the `Shape` class, then this doesn't scale all that well - though in some senses my solution is the natural extension of this approach as it passes around the entire class dictionary with each value which you can then apply as you want.
Ganesh Sittampalam
Sure, this only works if `Shape` has nothing more than `draw`. When I started writing this I had a `Drawable` _data type_, with a single `IO ()` field, and a `toDrawable :: (Shape a) => a -> Drawable` function. That would be extensible to more fields (for each function in `Shape`) and that of course is just a manually constructed class dictionary...
Tom Lokhorst
Ganesh's comment was my reaction to this as well. It does show a really interesting use of Haskell's lazy evaluation. That's kind of cool. Being new to Haskell, I have to admit that it's going to take me a bit to get used to taking advantage of lazy evaluation.
Clint Miller
+3  A: 

One way to do it would be with vtables:

data Shape = Shape {
  draw :: IO (),
  etc :: ...
}

rectangle :: Int -> Int -> Shape
rectangle len width = Shape {
  draw = ...,
  etc = ...
}

circle :: Int -> Int -> Shape
circle center radius = Shape {
  draw = ...,
  etc = ...
}
yairchu
The problem here is that your Shape data type has to be a union of all the fields of all possible shapes. That may work fine for my (contrived) case, but it will be an awkward approach in more complex cases.
Clint Miller
Interesting answer, hadn't thought of something like this. @Clint: I don't think the `Shape` data type here has be be the union of all possible fields. It describes all the functions in your `Shape` class, each constructor function (rectangle, circle) constructs a shape by providing a unique implementation for the functions, just like with instances.
Tom Lokhorst
This is basically working with explicit type class dictionaries. It works, but most of the time implicit dictionaries are much simpler. The existential-based solution I posted will end up being the same as this under the covers (at least with a compiler like GHC that uses dictionaries to implement type classes).
Ganesh Sittampalam
I get it. I mis-read this the first time. The rectangle and circle functions return Shape objects with functions that are closures over len and width or center and radius. Pretty clever.
Clint Miller