type classes vs. multiple data constructors

Andrew J Bromage ajb@spamcop.net
Tue, 18 Feb 2003 11:33:44 +1100


G'day.

On Mon, Feb 17, 2003 at 01:44:07AM -0500, Mike T. Machenry wrote:

>  I was wondering if it's better to define them as type classes with the
> operations defined in the class. What do haskellian's do?

I can't speak for other Haskellians, but on the whole, it depends.

Here's the common situation: You have a family of N abstractions.  (In
your case, N=2.)  The abstractions are similar in some ways and
different in some ways.  The most appropriate design depends largely
on what those similarities and differences are.

>From your definitions, it seems clear that there is some common
structure.  That suggests that a vanilla type class solution may
not be appropriate, because type classes do not directly support
common structure, only related operations.

Your solution wasn't bad:

> data PlayerState =
>   FugitiveState {
>     tickets     :: Array Ticket Int,
>     fHistory     :: [Ticket] } |
>   DetectiveState {
>     tickets     :: Array Ticket Int,
>     dHistory     :: [Stop] }

If the operations on the "tickets" field are different, or the
algorithms which operate on PlayerState are different for the
FugitiveState case and the DetectiveState case, this may be a good
design, because by not sharing structure, you're explicitly denying
any similarity (i.e. just because look the similar, that doesn't mean
they are similar).

If they are similar, then this may not be an appropriate design.
Ideally, you want to use language features and/or idioms which expose
the similarities (where they exist) and the differences (where they
exist).

Here's one design where the structural similarity is explicit:

	data PlayerState
	  = PlayerState {
		tickets :: Array Ticket Int,
		role    :: RoleSpecificState
	    }

	data RoleSpecificState
	  = FugitiveState  { fHistory :: [Ticket] }
	  | DetectiveState { dHistory :: [Stop] }

Depending on how similar the operations on the RoleSpecificState are
(say, if they are related by a common type signature, but have little
code in common), or if you want a design which is extensible to other
kinds of player (possibly at dynamic-link time) you may prefer to use
type classes to implement the role-specific states instead:

	-- Warning: untested code follows

	class RoleSpecificState a where
		{- ... -}

	data FugitiveState = FugitiveState { fHistory :: [Ticket] }

	instance RoleSpecificState FugitiveState where
		{- ... -}

	data DetectiveState = DetectiveState { fHistory :: [Ticket] }

	instance RoleSpecificState DetectiveState where
		{- ... -}

	data PlayerState
	  = forall role. (RoleSpecificState role) =>
	    PlayerState {
		tickets :: Array Ticket Int,
		role    :: role
	    }

(If you know anything about design patterns, you may recognise this as
being similar to the "Strategy" pattern.  This is no accident.)

It's hard to say what is the most appropriate design without looking at
the algorithms and operations which act on the PlayerState type and
analysing their similarities and differences.

> Oh and if I say
> Instance Foo Baz where
>   ...
> 
> and only define a few of the operations in Foo... bdoes Baz take on some
> default methods?

If you've declared default methods, yes.

Cheers,
Andrew Bromage