If you want better flexibility and composability, then reify the type classes as a record. RankNTypes may be necessary.
For example, here's how you could reify the Applicative type class
{-# LANGUAGE RankNTypes #-}
data ApplicativeInstance f = ApplicativeInstance
{ pure :: forall a. a -> f a
, amap :: forall a b. (a -> b) -> f a -> f b
, ap :: forall a b. f (a -> b) -> f a -> f b
}
listApplicative = ApplicativeInstance
{ pure = \a -> [a]
, amap = map
, ap = \fs xs -> case fs of
[] -> []
f:fs' -> map f xs ++ ap cartesianListApplicative fs' xs
}
zipListApplicative = ApplicativeInstance
{ pure = \a -> [a]
, amap = map
, ap = \fs xs -> case (fs, xs) of
([], _) -> []
(_, []) -> []
(f:fs', x:xs') -> f x : ap zipListApplicative fs' xs'
}
Now, we gain the power of specifying which instance we want. However, we lose the power to implicitly choose an instance: selection must now be explicit.
ghci> ap listApplicative [(+1), (*3)] [1 .. 5]
[2,3,4,5,6,3,6,9,12,15]
ghci> ap zip
zip zipListApplicative zipWith3
zip3 zipWith
ghci> ap zipListApplicative [(+1), (*3)] [1 .. 5]
[2,6]
See also:
http://lukepalmer.wordpress.com/2010/01/24/haskell-antipattern-existential-typeclass/