[Haskell-cafe] Refactoring type-class madness

wren ng thornton wren at freegeek.org
Fri Jul 16 01:07:00 EDT 2010


Andrew Webb wrote:
> Because, at the basic level all of the experiments share this type of
> data, it seems that I should be able to write analysis functions that
> work for any experiment. However, the experiments differ in the
> stimuli used, and associated with each stimulus set is a set of
> "milestones" that give times at which important things happen in the
> stimuli, and "regions of interest" that give areas of the visual scene
> that are considered important. For a single experiment I would have:
> 
> data Experiment = Exp [Trial]
> data Trial = Trial [Event] (Map MileStone Time)
> data MileStone = M1 | M2 | ...
> 
> [...]
> 
> So, I was wondering whether there was something wrong with my basic
> model which leads to this ugly type class, or whether this is the
> proper way forward. Either is fine, really, it would just be nice to
> know for certain.

In sounds like you're trying to use typeclasses as if they were 
OO-classes, which is a good way to confuse yourself. What you probably 
want is just parametric polymorphism. For example, if every experiment 
is a sequence of trials, and every trial is a sequence of events with 
some milestones, then you can get the generality you want with:

     data Experiment m = Exp [Trial m]
     data Trial m = Trial [Event] (Map m Time)
     data Event = Fixation {...} | Saccade {...}

     data A = MS_A1 | MS_A2 | ... deriving (Ord, Enum)
     data B = MS_B1 | MS_B2 | ... deriving (Ord, Enum)
     ...

then you would pass around (Experiment A), (Experiment B), etc. The 
reason for the Ord instances is so you can use them as keys in Map, and 
the reason for Enum is just so you have a generic interface for listing 
all the milestones (though getting the keys of the map may suffice).

The main reason for wanting to use typeclasses is when you have a common 
interface (i.e. set of function names and types), but the 
implementations of that interface are structurally/algorithmically 
different. If the structure of the implementation is the same and only 
the type of some component changes, then parametric polymorphism is the 
way to go.


Off-topic to your original question, it seems like a better model for 
your data might be to treat milestones as a third kind of event. Thus, a 
trial is just a sequence of events, which could be subject events 
(fixation, saccades) or experimental events (milestones, etc). This is 
assuming that your processing only cares about how patient events occur 
relative to experimental events, and that you don't need access to 
experimental events separately.

If you need to be able to jump around to different events, then you 
could use Trial[Event](Map m [Event]) and construct the map after 
reading input by walking over the list of events and storing pointers to 
the subsequence beginning with each milestone:

     computeMilestones :: Trial m -> Trial m
     computeMilestones (Trial es m) = Trial es (go es m)
         where
         go []         m = m
         go es@(e:es') m = go es' (m' e)
             where
             m' (MS x) = insert x es m
             m' _      = m


-- 
Live well,
~wren


More information about the Haskell-Cafe mailing list