[Haskell-cafe] Polymorphic (typeclass) values in a list?

Jules Bean jules at jellybean.co.uk
Fri Oct 19 12:32:10 EDT 2007


TJ wrote:
> Why is it illegal to store values of differing types, but which
> instance the same class, into a list? e.g.
> 
> a = [ 1, 2.0 ] :: Num a => [a]

That type signature doesn't mean what you want it to mean. That reads

"A list of things of type a ([a]) with the restriction that the type a 
is a member of the Num class"

> After all, sometimes all you need to know about a list is that all the
> elements support a common set of operations. If I'm implementing a 3d
> renderer for example, I'd like to have

That's a reasonable thing to want.

> 
> class Renderable a where
>   render :: a -> RasterImage
> 
> scene :: Renderable a => [a]
> 
> 
> Instead of hardcoding a bunch of types as being Renderable, as in
> 
> data Renderable
>   = Point Something
>   | Line Something
>   | Polygon Something
> 
> scene :: [Renderable]

Quite often an explicit ADT is much nicer. But they represent two 
opposing patterns of code-writing. Explicit ADT allows you to write case 
statements handling 'all the logic in one place'; a class forces you to 
separate the differences into 'separate instances'.

Both styles have their place. It's partly a question of taste, partly an 
issue of drawing your abstraction boundaries at different angles!

This is how you do it:

class CanRender a where render :: a -> RasterImage

data Renderable = forall a. (CanRender a) => Renderable a

scene :: [Renderable]

> I even if I use those, I'll still have to wrap things up in a
> constructor, won't I?

Yes. It's not as onerous as all that though. You can normally set things 
up with pleasant helper functions.

One helpful trick is to make the existential-type itself a member of the 
class, as in:

instance CanRender Renderable where render (Renderable a) = render a

I think it's interesting to note, in passing, that you don't have to use 
typeclasses. It can be perfectly useful to use existentials over other 
structures, like:

data Particle s = Particle { x :: Double,
                              y :: Double,
                              z :: Double,
                              state :: s,
                              drawme :: Particle s -> IO () }

...which is some representation of a 'particle' but it is parametric in 
some generic kind of state. Then if you don't care which state, you can do

data AnyParticle = forall s . Any (Particle s)

Including the last member 'drawme' makes this technique look rather like 
'dictionaries-by-hand'. Which it is. And 'dictionaries-by-hand' is 
actually much nicer than type classes. The point is : just passing 
higher-order functions around in your data structures is a very useful 
trick. Taking existentials of these can be useful too. The *only* time 
it's useful to make this paradigm into a type-class, is when you want 
the compiler to automatically provide the dictionary for you, based on 
the types you use.

Since I can imagine several different kinds of particle which might use 
'int' as state , but draw themselves differently, type-based dictionary 
choice is not the be-all and end-all. If you really really want to use 
type-based dictionary choice you can of course use newtypes... but it 
pays to remember there is a more elementary approach.


IF you draw the analogy between existentials and OO programming, then 
you might say the following:

Typeclass dictionaries are like class-based OO (e.g. Java) where to 
define a new kind of behaviour you need to make a new class.

Dictionaries-by-hand is like object-based OO (e.g. javascript) where to 
define a new kind of behaviour you can just change a method, one-off, 
for a particular object or bunch of objets.

However, I prefer to think that this is all about different kinds of 
abstraction. Java is at one extreme of the spectrum, with objects and 
classes being its only tools of abstraction (all code goes in classes, 
all data goes in objects). With Haskell the various parts of the 
abstraction are split up : custom data types, higher order functions, 
polymorphic functions, existential types. This means (with a little 
experience) you can apply the right tool for the job.

Jules



More information about the Haskell-Cafe mailing list