-->

Haskell, Gen instance of B when class A provides e

2019-02-26 19:11发布

问题:

While writing a class for a Collection/Container type (btw point me towards existing types if i'm reinventing the wheel) to provide a general interface for adding and removing elements from any 'Collection' type.

class (Eq (c a), Monoid (c a)) => Collection c a where
  emptyColl   :: c a -> Bool
  splitColl   :: c a -> (a, c a)
  toColl      :: a -> c a
  size        :: c a -> Int
  combineColl :: a -> c a -> c a
  (>|<)       :: a -> c a -> c a
  a >|< c     =  combineColl a c

I noticed instances of Collection could also be instances of Foldable. Using splitColl and emptyColl. So you don't need to write a Foldable as well, if you make constructs like this over several classes a significant amount of time, writing trivial instances, can be saved.

I tried to make Collection an instance of Foldable. However classes can't seem to be instantiated from other classes do they? I received the following error messages:

instance Functor (Collection c) where

The first argument of ‘Functor’ should have kind ‘* -> *’,
  but ‘Collection c’ has kind ‘* -> GHC.Prim.Constraint’
In the instance declaration for ‘Functor (Collection c)’ 

instance Functor (Collection c a) where

 The first argument of ‘Functor’ should have kind ‘* -> *’,
  but ‘Collection c a’ has kind ‘GHC.Prim.Constraint’
In the instance declaration for ‘Functor (Collection c a)’

How could I get the desired functionality? I think template Haskell might be the way to go here, I never used it tho, an example would be great :)

Thanks in advance!

PS: I've been writing haskell for about a year, minor tips that come to mind are much appreciated

回答1:

The standard trick for this is to give implementations of the appropriate functions and let users write their own instances. For example, you might write

fmapColl :: (Collection c a, Collection c b) => (a -> b) -> c a -> c b
fmapColl f ca
    | emptyColl ca = mkEmptyColl -- you don't have this in your class, but probably should
    | otherwise = case splitColl ca of
        (a, ca') -> f a >|< fmapColl f ca'

Assuming we had a suitable type class, say, CFunctor:

class CFunctor f where
    type ConstraintI f
    type ConstraintO f
    cfmap :: (ConstraintI i, ConstraintO o) => (i -> o) -> f i -> f o

Then for a given Collection instance like Set we could instantiate CFunctor with minimal actual code like this:

instance CFunctor Set where
    type ConstraintI Set = Ord
    type ConstraintO Set = Ord
    cfmap = fmapColl

You can see this pattern -- defining a "default" implementation for users to put in their instances -- in the base library's fmapDefault, for example.