330 likes | 460 Views
Scrap your boilerplate with class. Ralf L ä mmel, Simon Peyton Jones Microsoft Research. The seductive dream: customisable generic programming. Define a function generically “gsize t = 1 + gsize of t’s children”
E N D
Scrap your boilerplate with class Ralf Lämmel, Simon Peyton Jones Microsoft Research
The seductive dream: customisable generic programming • Define a function generically“gsize t = 1 + gsize of t’s children” • Override the generic defn at specific types“gsize of a string is the length of the string” • Use the generic function at any type“gsize <complicated data structure>”
1. Generic definition [TLDI’03] gsize :: Data a => a -> Int gsize t = 1 + sum (gmapQ gsize t) class Dataa where gmapQ :: (forall b. Data b => b -> r) -> a -> [r]-- (gmapQ f t) applies f to each of t’s-- children, returning list of results • NB: Cool higher rank type for gmapQ
Need Data instance for each type (once and for all) • Higher rank type class Dataa where gmapQ :: (forall b. Data b => b -> r) -> a -> [r]-- (gmapQ f t) applies f to each of t’s-- children, returning list of results instance Data Int wheregmapQ f i = [] instance Data a => Data [a] wheregmapQ f [] = []gmapQ f (x:xs) = [f x, f xs]
The seductive dream: customisable generic programming • Define a function generically“gsize t = 1 + gsize of t’s children” • Override the generic defn at specific types“gsize of a string is the length of the string” • Use the generic function at any type“gsize <complicated data structure>” Done!
Override gsize at specific type • Plan A: dynamic type test [TLDI’03] gsizeString :: [Char] -> Int gsizeString s = length s gsize :: Data a => a -> Int gsize = (\t -> 1 + sum (gmapQ gsize t)) `extQ` gsizeString
The seductive dream: customisable generic programming • Define a function generically“gsize t = 1 + gsize of t’s children” • Override the generic defn at specific types“gsize of a string is the length of the string” • Use the generic function at any type“gsize <complicated data structure>” Done! Done!
Not quite... • Problems with Plan A • Dynamic type test costs • No static check for overlap • Fiddly for type constructors [ICFP’04] • Worst of all: tying the knot prevents further extension gsize :: Data a => a -> Int gsize t = (1 + sum (gmapQ gsize t)) `extQ` gsizeString
Tantalising Plan B: type classes class Size a where gsize :: a -> Int instance Size a => Size [a] where gsize xs = length xs • Can add new types, with type-specific instances for gsize, “later” • No dynamic type checks • Plays nicely with type constructors
...BUT • Boilerplate instance required for each new type, even if only the generic behaviour is wanted data MyType a = MT Int a instance Size a => Size (MyType a) where gsize (MT i x) = 1 + gsize i + gsize x data YourType a = YT a a instance Size a => Size (YourType a) where gsize (YT i j) = 1 + gsize i + gsize j
The seductive dream: customisable generic programming • Define a function generically“gsize t = 1 + gsize of t’s children” • Override the generic defn at specific types“gsize of a string is the length of the string” • Use the generic function at any type“gsize <complicated data structure>” Undone! Done better!
Writing the generic code Why can’t we combine the two approaches, like this? Generic case class Size a where gsize :: a -> Int instance Data t => Size t wheregsize t = 1 + sum (gmapQ gsize t) instance Size a => Size [a] where ... More specific cases over-ride
...utter failure instance Data t => Size t wheregsize t = 1 + sum (gmapQ gsize t) gmapQ :: Data a => (forall b. Data b => b -> r) -> a -> [r] gsize :: Size b => b -> Int (gmapQ gsize t) will give a Data dictionary to gsize... ...but alas gsize needs a Size dictionary
Idea (bad) Make a Data dictionary contain a Size dictionary class Size a => Data a wheregmapQ :: (forall b. Data b => b -> r) -> a -> [r] Now the instance “works”... but the idea is a non-starter: For every new generic function, we’d have to add a new super-class to Data, ...which is defined in a library
Main idea of the talk Much Better Idea Parameterise over the superclass: [Hughes 1999]Data dictionary contains a cxt dictionary class (cxt a) => Data cxt a wheregmapQ :: (forall b. Data cxt b => b -> r) -> a -> [r] • ‘cxt’ has kind ‘*->pred’ • just as • ‘a’ has kind ‘*’
Much Better Idea [nearly] works instance Data Size t => Size t wheregsize t = 1 + sum (gmapQ gsize t) gmapQ :: Data cxt a => (forall b. Data cxt b => b -> r)-> a -> [r] gsize :: Size b => b -> Int (gmapQ gsize t) will give a (Data Size t) dictionary to gsize... ...and gsize can get the Size dictionary from inside it
The seductive dream: customisable generic programming • Define a function generically“gsize t = 1 + gsize of t’s children” • Override the generic defn at specific types“gsize of a string is the length of the string” • Use the generic function at any type“gsize <complicated data structure>” Done again! Done better!
Story so far • We can write a generic program once class Size a wheregsize :: a -> Int instance Data Size t => Size t where ... Later, define a new type data Wibble = ... deriving( Data ) Optionally, add type-specific behaviour instance Size Wibble where ... In short, happiness: regular Haskell type-class overloading plus generic definition
Things I swept under the carpet • Type inference fails • Haskell doesn’t have abstraction over type classes • Recursive dictionaries are needed
Type inference fails (Data Size t) dictionary available... instance Data Size t => Size t wheregsize t = 1 + sum (gmapQ gsize t) ...but no way to know thatcxt = Size (Data cxt t) dictionary required... gmapQ :: Data cxt a => (forall b. Data cxt b => b -> r) -> a -> [r]
Type inference fails instance Data Size t => Size t wheregsize t = 1 + sum (gmapQ gsize t) We really want to specify that cxt should be instantiated by Size, at this call site gmapQ :: Data cxt a => (forall b. Data cxt b => b -> r) -> a -> [r]
Type proxy value argument instance Data Size t => Size t wheregsize t = 1 + sum (gmapQ gsProxy gsize t) data Proxy (cxt :: *->pred) gsProxy :: Proxy Size gsProxy = error “urk” Type-proxy argument Type-proxy argument gmapQ :: Data cxt a => Proxy cxt -> (forall b. Data cxt b => b -> r) -> a -> [r]
Things I swept under the carpet Done!(albeit still tiresome) • Type inference fails • Haskell doesn’t have abstraction over type classes • Recursive dictionaries are needed
Recursive dictionaries instance (Data cxt a, cxt[a]) => Data cxt [a] wheregmapQ f [] = [] gmapQ f (x:xs) = [f x, f xs] (I1) • Need (Size [Int]) • Use (I2) to get it from (Data Size [Int]) • Use (I1) to get that from (Data Size Int, Size [Int]) instance Data Size t => Size t wheregsize t = 1 + sum (gmapQ gsize t) (I2)
Recursive dictionaries i1 :: (Data cxt a, cxt [a]) -> Data cxt [a] i2 :: Data Size t -> Size t i3 :: Data cxt Int • Need (Size [Int]) • Use (I2) to get it from (Data Size [Int]) • Use (I1) to get that from (Data Size Int, Size [Int]) rec d1::Size [Int] = i2 d2 d2::Data Size [Int] = i1 (d3,d1) d3::Data Size Int = i3
Recursive dictionaries • Recursive dictionaries arise naturally from solving constraints co-inductively • Coinduction: to solve C, assume C, and then prove C’s sub-goals • Sketch of details in paper; formal details in [Sulzmann 2005]
Things I swept under the carpet Done! • Type inference fails • Haskell doesn’t have abstraction over type classes • Recursive dictionaries are needed Done!
Encoding type-class abstraction Wanted (cxt::*->pred) class (cxt a) => Data cxt a wheregmapQ :: (forall b. Data cxt b => b -> r) -> a -> [r] Encoding (cxt::*->*) class Sat (cxt a) => Data cxt a wheregmapQ :: (forall b. Data cxt b => b -> r) -> a -> [r] class Sat a wheredict :: a
Encoding type-class abstraction Wanted (Size::*->pred) instance Data Size t => Size t wheregsize t = 1 + sum (gmapQ gsize t) Encoding (SizeD::*->*) instance Data SizeD t => Size t wheregsize t = 1 + sum (gmapQ (gsizeD dict) t) data SizeD a = SD (a -> Int) gsizeD (SD gs) = gs instance Size a => Sat (SizeD a) wheredict = SD gsize
Encoding type-class abstraction • Details straightforward. It’s a little fiddly, but not hard • A very cool trick • Does Haskell need native type-class abstraction?
SYB home page:http://www.cs.vu.nl/boilerplate/ Summary • A smooth way to combine generic functions with the open extensibility of type-classes • No dynamic type tests, although they are still available if you want them, via (Data Typeable a) • Longer case study in paper • Language extensions: • coinductive constraint solving (necessary) • abstraction over type classes (convenient)
Recursive dictionaries Known instances Constraint to be solved Solve( S, C ) = Solve( S, D1 ) if S contains... instance (D1..Dn) => CSolve( S, Dn )
Recursive dictionaries Known instances Constraint to be solved Solve( S, C ) = Solve( S C, D1 ) if S contains... instance (D1..Dn) => CSolve( S C, Dn ) Coinduction: to solve C, assume C, and then prove C’s sub-goals (cf Sulzmann05)