Proper Treatment 正當作法/ blog/ posts/ The extensible visitor pattern in tagless final style
標籤 Tags:
2011-01-26 02:05

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.)