This literate Haskell program translates Shriram Krishnamurthi, Matthias Felleisen, and Daniel P. Friedman’s “extensible visitor pattern”. Their paper “Synthesizing object-oriented and functional design to promote re-use” (ECOOP 1998, 91–113) proposes this pattern as a solution to the expression problem. Unlike them, we don’t use any type casts!
{-# LANGUAGE Rank2Types #-} module ExtensibleVisitor where data Point = Point { x, y :: Double } deriving (Eq, Show) class ShapeProcessor a where square :: Double {-s-} -> a circle :: Double {-r-} -> a translated :: Point {-d-} -> a {-s-} -> a newtype Render = Render (Int {-prec-} -> String -> String) instance Show Render where showsPrec p (Render s) = s p instance ShapeProcessor Render where square s = Render (\prec -> showParen (prec > 10) (showString "square " . showsPrec 11 s)) circle r = Render (\prec -> showParen (prec > 10) (showString "circle " . showsPrec 11 r)) translated d (Render s) = Render (\prec -> showParen (prec > 10) (showString "translated " . showsPrec 11 d . showChar ' ' . s 11))
> translated (Point 1 2) (circle 3) ::
Render
translated (Point {x = 1.0, y = 2.0}) (circle 3.0)
newtype ContainsPt = ContainsPt { containsPt :: Point {-p-} -> Bool } instance ShapeProcessor ContainsPt where square s = ContainsPt (\(Point x y) -> 0 <= x && x <= s && 0 <= y && y <= s) circle r = ContainsPt (\(Point x y) -> x * x + y * y <= r * r) translated (Point dx dy) s = ContainsPt (\(Point x y) -> containsPt s (Point (x - dx) (y - dy)))
> containsPt (translated (Point 1 2) (circle 3))
(Point 2 3)
True
newtype Shrink a = Shrink { shrink :: Double {-pct-} -> a } instance (ShapeProcessor a) => ShapeProcessor (Shrink a) where square s = Shrink (\pct -> square (s / pct)) circle r = Shrink (\pct -> circle (r / pct)) translated d s = Shrink (\pct -> translated d (shrink s pct))
> shrink (translated (Point 1 2) (circle 3)) 10
:: Render
translated (Point {x = 1.0, y = 2.0}) (circle 0.3)
class ShapeProcessor a => UnionShapeProcessor a where union :: a {-s1-} -> a {-s2-} -> a instance UnionShapeProcessor Render where union (Render s1) (Render s2) = Render (\prec -> showParen (prec > 10) (showString "union " . s1 11 . showChar ' ' . s2 11))
> translated (Point 1 2) (union (square 4)
(circle 3)) :: Render
translated (Point {x = 1.0, y = 2.0}) (union (square 4.0) (circle
3.0))
instance UnionShapeProcessor ContainsPt where union s1 s2 = ContainsPt (\p -> containsPt s1 p || containsPt s2 p)
> containsPt (translated (Point 1 2) (union
(square 4) (circle 3))) (Point 2 3)
True
instance (UnionShapeProcessor a) => UnionShapeProcessor (Shrink a) where union s1 s2 = Shrink (\pct -> union (shrink s1 pct) (shrink s2 pct))
> shrink (translated (Point 1 2) (union (square
4) (circle 3))) 10 :: Render
translated (Point {x = 1.0, y = 2.0}) (union (square 0.4) (circle
0.3))
> containsPt (shrink (translated (Point 1 2)
(union (square 4) (circle 3))) 10) (Point 2 3)
False
Below is the part that uses rank-2 types. We only need them if we want to process the output of a processor multiple times.
newtype Shape = Shape { processShape :: forall a. ShapeProcessor a => a } instance ShapeProcessor Shape where square s = Shape (square s) circle r = Shape (circle r) translated d s = Shape (translated d (processShape s)) newtype UnionShape = UnionShape { processUnionShape :: forall a. UnionShapeProcessor a => a } instance ShapeProcessor UnionShape where square s = UnionShape (square s) circle r = UnionShape (circle r) translated d s = UnionShape (translated d (processUnionShape s)) instance UnionShapeProcessor UnionShape where union s1 s2 = UnionShape (union (processUnionShape s1) (processUnionShape s2)) test :: UnionShape test = shrink (translated (Point 1 2) (union (square 4) (circle 3))) 10
> processUnionShape test :: Render
translated (Point {x = 1.0, y = 2.0}) (union (square 0.4) (circle
0.3))
> containsPt (processUnionShape test) (Point 2
3)
False
(It would be nice to overload the names
processShape and processUnionShape. We
can do that by reifying the type classes
ShapeProcessor and UnionShapeProcessor as
two types that belong to the same multiparameter type class.)