2
votes

I've written a simple typeclass Shape:

class Shape a where
  draw   :: a -> IO ()
  move   :: (Double,Double) -> a -> a
  area   :: a -> Double
  circum :: a -> Double

I also have concrete types Circle, Rect and Triangle that instantiate this typeclass like so:

data Circle = Circle Point Double deriving (Show)

instance Shape Circle where
  draw       (Circle centre radius) = putStrLn $ "Circle [" ++ show centre ++ ", " ++ show radius ++ "]"
  move (x,y) (Circle centre radius) = Circle (movePoint x y centre) radius
  area   (Circle _ r) = r ^ 2 * pi
  circum (Circle _ r) = 2 * r * pi

movePoint :: Double -> Double -> Point -> Point
movePoint x y (Point x_a y_a) = Point (x_a + x) (y_a + y)

In order to work with heterogenous lists containing instances of the concrete types Circle, Rect and Triangle I followed the haskell wiki tutorial on heterogenous collections and implemented an existential data type ShapeType like so:

{-# LANGUAGE ExistentialQuantification #-}

data ShapeType = forall a . Shape a => MkShape a

I let ShapeTypeinstantiate the Shape type class:

instance Shape ShapeType where
  area     (MkShape s) = area s
  circum   (MkShape s) = circum s
  draw     (MkShape s) = draw s
  move (x,y) (MkShape s) =  move (x,y) s  -- this does not compile

Now I can use this in code like follows:

rect = Rect (Point 0 0) (Point 5 4)
circle = Circle (Point 4 5) 4
triangle = Triangle (Point 0 0) (Point 4 0) (Point 4 3)

shapes :: [ShapeType]
shapes = [MkShape rect, MkShape circle, MkShape triangle]

main = do
  print $ map area shapes
  print $ map circum shapes
  mapM_ draw shapes

Unfortunately this only compiles if I ommit the line

move (x,y) (MkShape s) = move (x,y) s

Otherwise I get the following compilation error:

error:
    * Couldn't match expected type `ShapeType' with actual type `a'
      `a' is a rigid type variable bound by
        a pattern with constructor:
          MkShape :: forall a. Shape a => a -> ShapeType,
        in an equation for `move'
        at C:\\workspace\FPvsOO\src\Lib.hs:102:15-23
    * In the expression: move (x, y) s
      In an equation for `move': move (x, y) (MkShape s) = move (x, y) s
      In the instance declaration for `Shape ShapeType'
    * Relevant bindings include
        s :: a (bound at C:\\workspace\FPvsOO\src\Lib.hs:102:23)

This doesn't make sense to me, as "extracting" the s by pattern matching for usage in the delegating calls works fine in the other three cases.

Any ideas what's going wrong here?

Update

With this simple fix the code now works as expected:

instance Shape ShapeType where
  area     (MkShape s) = area s
  circum   (MkShape s) = circum s
  draw     (MkShape s) = draw s
  move vec (MkShape s) = MkShape (move vec s)
1
A good alternative to existentials here is a record of functions: data S = S { draw ∷ IO (), move ∷ (Double, Double) → S, … }. Shapes are defined as structural compositions of behaviour rather than nominal types in a typeclass: circle c r = S { draw = putStrLn "Circle" …, move = \(x, y) → circle (movePoint x y c) r, … }. The closure capturing fields like c & r is already existential, but you can also explicitly hide fields existentially without a typeclass: data S where { S ∷ ∀ s. { fields ∷ s, draw ∷ s → IO (), move ∷ (Double, Double) → s → s, … } → S }.Jon Purdy

1 Answers

4
votes

You missed a constructor. You need

move v (MkShape s) = MkShape $ move v s

I'm not so convinced your approach here is really best; existential types tend to just gum up the works in this sort of situation. You should at least consider a plain old sum type. Existentials are invaluable for some purposes, but in other contexts they're actively harmful.