Ryan Kennedy
Ryan Kennedy

Reputation: 3647

Use instance type parameter in type class

As an exercise, I'm trying to create a Vector typeclass as an exercise:

class Vector v where
  vplus :: v -> v -> v
  vnegate :: v -> v

type V3 a = (a,a,a)

instance (Num a) => Vector (V3 a) where
  (a,b,c) `vplus` (d,e,f) = (a+d, b+e, c+f)
  vnegate (a,b,c) = ((-a), (-b), (-c))

I want to add a dot function on the typeclass. On the V3 example above, I'd implement it as follows:

dot :: (Num a) => V3 a -> V3 a -> a
(a,b,c) `dot` (d,e,f) = a*d + b*e + c*f

However, it appears I don't have access to the type parameter a from within Vector, so I can't have dot operate over Vector the way I want. How would I access the a type parameter?

Upvotes: 2

Views: 322

Answers (2)

Daniel Wagner
Daniel Wagner

Reputation: 153342

Another solution, which requires no extensions, is to use a higher-kinded class. Thus:

class Vector v where
    vplus :: Num a => v a -> v a -> v a
    vnegate :: Num a => v a -> v a

Then it's easy to add a dot-product method:

    dot :: Num a => v a -> v a -> a

The instance method implementations won't have to change, though the instance declaration itself would have to change:

instance Vector V3 where
    -- method implementations are the same as before

Upvotes: 3

bheklilr
bheklilr

Reputation: 54078

You want to use TypeFamilies for this to create an associated type:

{-# LANGUAGE TypeFamilies, TypeSynonymInstances, FlexibleInstances #-}

class Vector v where
    -- Declares a family of types called Item, parametrized on the
    -- instance v of Vector, and the kind of Item v must be *,
    -- meaning that it must be a type, not a type constructor
    -- (e.g. Maybe Int :: * vs Maybe :: * -> *)
    type family Item v :: *
    dot :: v -> v -> Item v
    ...

instance (Num a) => Vector (V3 a) where
    type Item (V3 a) = a
    dot (a, b, c) (d, e, f) = a*d + b*e + c*f
    ...

Then you can do

> dot (1, 2, 3) ((4, 5, 6) :: V3 Int)
32

Although I would recommend against using a type synonym instance, you'd be better off using a data type:

data V3 a = V3 a a a deriving (Eq, Show)

instance Functor V3 where
    fmap f (V3 a b c) = V3 (f a) (f b) (f c)

instance (Num a) => Vector (V3 a) where
    type Item (V3 a) = a
    (V3 a b c) `vplus` (V3 d e f) = V3 (a + d) (b + e) (c + f)
    vnegate v = fmap negate v
    dot (V3 a b c) (V3 d e f) = a*d + b*e + c*f

This helps out the type checker a lot, in particular it means you wouldn't need the explicit type signature above. It also means your inferred types won't be (a, a, a) but V3 a (like when you see [Char] instead of String), which is easier to follow. It's not crucial, but helpful.

In case you're wondering, this is how GHC.Exts.IsList (for use with the new OverloadedLists extension) does it:

class IsList l where
    type family GHC.Exts.Item l :: *
    fromList :: [GHC.Exts.Item l] -> l
    fromListN :: Int -> [GHC.Exts.Item l] -> l
    toList :: l -> [GHC.Exts.Item l]

Upvotes: 1

Related Questions