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 ShapeType
instantiate 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)
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 likec
&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