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.