
Tillmann,
Thank you for your detailed reply. It was a real eye opener. I
hadn't seen anything like that before.
It seems that your ShapeClass is very similar to, and plays the same
role as, the Class ShapeC from my example. I wonder if that was how
haskellers implemented shared functions before type classes were
invented.
One advantage that I see in your approach is that you only need one
function, "call", that can be used to dereference any method in
ShapeClass. In my example, I needed to define ShapeC ShapeD instances
for both draw and copyTo.
I suppose one nice aspect of using a type class is that the copyTo
method can be applied to a Rectangle to give another Rectangle, or to
a Circle, or to a generic ShapeD to give a generic ShapeD. The copyTo
function in your example produces a generic shape.
Thanks again for your help.
Tad
On Wed, Mar 30, 2011 at 2:57 AM, Tillmann Rendel
Hi,
Steffen Schuldenzucker wrote:
data Shape = Shape { draw :: String copyTo :: Double -> Double -> Shape }
Tad Doxsee wrote:
Suppose that the shape class has 100 methods and that 1000 fully evaluated shapes are placed in a list.
The above solution would store the full method table with each object. Instead, we could share the method tables between objects. An object would then uniformly contain two pointers: One pointer to the method table, and one poiner to the internal state.
{-# LANGUAGE ExistentialQuantification, Rank2Types #-}
data Object methods = forall state . Object { methods :: methods state, state :: state }
Calling a method requires dereferencing both pointers.
call :: (forall state . methods state -> state -> a) -> (Object methods -> a) call method (Object methods state) = method methods state
Using this machinery, we can encode the interface for shapes.
data ShapeClass state = ShapeClass { draw :: state -> String, copyTo :: state -> Double -> Double -> Shape }
type Shape = Object ShapeClass
An implementation of the interface consists of three parts: A datatype or the internal state, a method table, and a constructor.
data RectangleState = RectangleState {rx, ry, rw, rh :: Double}
rectangleClass :: ShapeClass RectangleState rectangleClass = ShapeClass { draw = \r -> "Rect (" ++ show (rx r) ++ ", " ++ show (ry r) ++ ") -- " ++ show (rw r) ++ " x " ++ show (rh r), copyTo = \r x y -> rectangle x y (rw r) (rh r) }
rectangle :: Double -> Double -> Double -> Double -> Shape rectangle x y w h = Object rectangleClass (RectangleState x y w h)
The analogous code for circles.
data CircleState = CircleState {cx, cy, cr :: Double}
circleClass :: ShapeClass CircleState circleClass = ShapeClass { draw = \c -> "Circ (" ++ show (cx c) ++ ", " ++ show (cy c)++ ") -- " ++ show (cr c), copyTo = \c x y -> circle x y (cr c) }
circle :: Double -> Double -> Double -> Shape circle x y r = Object circleClass (CircleState x y r)
Rectangles and circles can be stored together in usual Haskell lists, because they are not statically distinguished at all.
-- test r1 = rectangle 0 0 3 2 r2 = rectangle 1 1 4 5 c1 = circle 0 0 5 c2 = circle 2 0 7
shapes = [r1, r2, c1, c2]
main = mapM_ (putStrLn . call draw) shapes
While this does not nearly implement all of OO (no inheritance, no late binding, ...), it might meet your requirements.
Tillmann
PS. You could probably use a type class instead of the algebraic data type ShapeClass, but I don't see a benefit. Indeed, I like how the code above is very explicit about what is stored where. For example, in the code of the rectangle function, it is clearly visible that all shapes created with that function will share a method table.