简体   繁体   中英

Haskell Data types with context

I want to write base implementation for Vertex.

data Point a = Point a a

class XY c where

    x :: c a -> a

    y :: c a -> a

class XY c => Vertex c where

    translate :: c a -> c a -> c a

    scale :: a -> c a -> c a

    rotate :: a -> c a -> c a

instance XY Point where

    x (Point first second) = first

    y (Point first second) = second

instance Vertex Point where

    translate xy1 xy2 = Point (x xy1 + x xy2) (y xy1 + y xy2)

    scale a xy = Point ((*a) $ x xy) ((*a) $ y xy)

    rotate a xy = Point (x * cosA - y * sinA) (x * sinA + y * cosA) where
                    cosA = cos a
                    sinA = sin a

I have to create instance of typeclass Vertex with implementation of Floating typeclass in Point type parameter. If i implement it like instance (Floating a) => Vertex Point a where i get:

 Expected kind ‘* -> Constraint’,
        but ‘Vertex Point’ has kind ‘Constraint’

What is the correct way to write it in Haskell?

Aww. This well-known problem is a pet peeve of mine. The correct™ solution is to make the XY and Point classes not for parametric types. The scalar argument becomes an associated type synonym, and everything works easily:

{-# LANGUAGE TypeFamilies #-}

class XY p where
  type Component p :: *
  x :: p -> Component p
  y :: p -> Component p

class XY p => Vertex p where
  translate :: p -> p -> p
  scale :: Component p -> p -> p
  rotate :: Component p -> p -> p

NB In fact you could even consider simplifying this to always use the same component type, since you'll likely never need anything else:

 class XY p where x :: p -> Double y :: p -> Double class XY p => Vertex p where translate :: p -> p -> p scale :: Double -> p -> p rotate :: Double -> p -> p 

With the non-parametric form, you can now easily add a number-type constraint exactly where it's needed, namely in the instance Vertex Point instance:

instance XY (Point a) where
  type Component (Point a) = a
  x (Point xc _) = xc
  y (Point _ yc) = yc

instance Floating a => Vertex (Point a) where
  translate xy1 xy2 = Point (x xy1 + x xy2) (y xy1 + y xy2)
  scale a xy = Point ((*a) $ x xy) ((*a) $ y xy)
  rotate a xy = Point (x * cosA - y * sinA) (x * sinA + y * cosA)
   where cosA = cos a
         sinA = sin a

For some reason , most people however prefer to make classes for geometric entities parametric over the scalar type, which is not only completely unnecessary but also un-geometric , because proper geometry is emphatically not depended of an actual basis decomposition.


Actually I'm fairly certain what the reason is: Edward Kmett's decision to use parameterised types in the linear library . He should have known better, especially since Conal Elliott's vector-space library , which does it the right way, has been around for longer already.

The following version is corrected so that it compiles:

data Point a = Point a a

class XY c where

    x :: c a -> a

    y :: c a -> a

class XY c => Vertex c where

    translate :: (Num a) => c a -> c a -> c a

    scale :: (Num a) => a -> c a -> c a

    rotate :: (Floating a) => a -> c a -> c a

instance XY Point where

    x (Point first second) = first

    y (Point first second) = second

instance Vertex Point where

    translate xy1 xy2 = Point (x xy1 + x xy2) (y xy1 + y xy2)

    scale a xy = Point ((*a) $ x xy) ((*a) $ y xy)

    rotate a xy = Point ((x xy) * cosA - (y xy) * sinA) ((x xy) * sinA + (y xy) * cosA) where
                    cosA = cos a
                    sinA = sin a

There were only 2 changes needed, in fact:

  • I have added type constraints on a for the methods of the XY class. Otherwise, you can't use functions such as + which you have in the implementation of the instance for Point . (GHC actually makes this exact suggestion in one of the error messages it throws when trying to compile your version.) Note that these have to go on the class, not the instance, because the instance declaration makes no mention of the type a (even though the implementation does). If you don't put the constraints in the class then the methods are expected to work for all possible types a .

  • x and y are in fact functions, so you can't multiply them with numbers like sinA . I suspect you just got confused here and could have figured out what to do - you needed to apply them to the xy (the "point" itself) to get the "x" and "y" "co-ordinates".

So actually you were pretty close, and just needed to pay attention to what the compiler was telling you. GHC's error messages can seem a bit obscure when you're new to Haskell, but with a bit of practice you soon see that they're (often, although not always) quite helpful.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM