330 likes | 344 Views
Explore the seductive dream of customisable generic programming through defining functions generically, overriding generic definitions for specific types, and utilizing generic functions across various data structures. Discover different approaches, challenges, and solutions for writing efficient and extensible generic code.
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)