Types vs. Classes
Jan-Willem Maessen
jmaessen at alum.mit.edu
Fri May 19 22:49:58 EDT 2006
On May 19, 2006, at 8:31 PM, Ashley Yakeley wrote:
> Occasionally in library proposals one comes across classes of this
> form:
>
> class Thingy a where
> foo :: a -> this
> bar :: a -> that
> spong :: a -> theotherthing
I was very entertained by the choice of "spong" as a random variable
name, as I know a prominent Spong...
> Such classes can be replaced by data-types, which turn out to also
> be more general:
>
> data Thingy = MkThingy
> {
> foo :: this,
> bar :: that,
> spong :: theotherthing
> }
>
> My question: is there a reason not to use types? I'm particularly
> interested in two cases that I've come across, references and
> streams. Here's the class version:
>
> class (Monad m) => Ref m r | r -> m, m -> r where
> newRef :: a -> m (r a)
> readRef :: r a -> m a
> writeRef :: r a -> a -> m ()
> ....
> And here's the type version:
>
> data Ref m a = MkRef
> {
> readRef :: m a,
> writeRef :: a -> m ()
> }
>
> class (Monad m) => HasRefs m where
> newRef :: a -> m (Ref m a)
> ...
> Is there any reason not to prefer using types?
This question set of some fascinating thoughts in my mind. So...
In favor of using data types:
* The object in question *is* its own dictionary. Instead of
passing two arguments to every function---a dictionary and a Ref,
say---you pass only one.
* No need to have a rigid choice of a single instance for a given
type. In principle you could write a wrapper of type (Ref m a -> Ref
m a) which gave you "logging refs", for example.
Countervailing factors are almost all efficiency arguments:
* You need to write both a type and a class in some cases (eg Ref)---
the only non-efficiency argument.
* Dictionaries are usually strict and unlifted (no need for laziness
or bottoms). They thus require rather less machinery at run time.
* Usually the record will be a series of closures over a single
object. These closures need to be allocated at run time and will
take up space. By contrast, the functions in a class don't require
runtime allocation, unless they are involved in complicated games
with polymorphic recursion. Even then, dictionaries can be shared.
* Compilers are pretty good at second-guessing the contents of
dictionary arguments. If it's the dictionary for Ref IO IORef, then
there's only one possible choice for newRef, readRef, and writeRef.
So if we know the type of the ref, we know which function we have to
call. With a relatively small amount of work, we can even specialize
functions for the actual dictionaries we pass in---so that if our
function is only ever used on IORefs, we plug in readIORef and
writeIORef directly. By contrast, we need to do some *very*
sophisticated pointer analysis to learn that our record of type Ref
IO IORef happens to always have a readRef field of the form
(readIORef ioRef) for some ioRef. This sort of analysis usually
requires having the entire program on hand, and tends to be brittle
even then since any heap analysis is flirting with undecidability.
Jhc is the only current compiler which does this, but it also uses a
very different dictionary representation which might well be more
efficient than the standard one anyhow.
This is really the flip side of the "flexibility" argument above. If
your representation is flexible, the implementation has to assume you
are going to use that flexibility until it can prove otherwise.
-Jan-Willem Maessen
> --
> Ashley Yakeley, Seattle WA
> WWEWDD? http://www.cs.utexas.edu/users/EWD/
>
> _______________________________________________
> Libraries mailing list
> Libraries at haskell.org
> http://www.haskell.org/mailman/listinfo/libraries
More information about the Libraries
mailing list