Metamorphosis
Metamorphosis copied to clipboard
TemplateHaskell functions to generate types and converter function.
- Overview =Metamorphosis= provides some Template Haskell functions to transform types and generate conversion function between the originals types and the metamorphosed ones. Conversions can be straightforward or done within an applicative functor allowing, traversing types but also automatic lifting of missing values. It allows for example to :
- split a type into many smaller ones
#+BEGIN_SRC haskell data AB = AB Int String #+END_SRC
to #+BEGIN_SRC haskell data A = A Int data B = B String #+END_SRC
- merge some types into a bigger one
#+BEGIN_SRC haskell data A = A Int data B = B String #+END_SRC
to
#+BEGIN_SRC haskell data AB = AB Int String #+END_SRC
- Changes the type of some fields
#+BEGIN_SRC haskell data Product = Product { name :: String, price :: Double } #+END_SRC
to #+BEGIN_SRC haskell data ProductM = ProductM { name :: String, price :: (Maybe Double) } #+END_SRC
- lift plain types to parametric ones
#+BEGIN_SRC haskell data Product = Product { name :: String, price :: Double } #+END_SRC
to #+BEGIN_SRC haskell data ProductF f = ProductF { name :: String, price :: f Double } #+END_SRC
etc ...
- Motivation I'm often confronted to the "extensible records" problem in Haskell. It can be reading two csv files and wanted to be able to join them as one type, without having to use tuples of them. It can be reading a csv (again) and wanting to validate it. For that I need some decoration around each field. The standard solution is to define a parametric type to decorate each field and use a type synonym to define the plain non-decorated type. However, I find it a bit heavy to have to pass around, a decorable type everywhere whereas, the decoration is only really need when reading/validating the data. Moreover, often, the non-decorated type is given has it is (sometime generated) and modifying it to add the parametric decoration is not an option. Another use case I encounter often, is when aggregating data, to create a Monoid instance, of type meant to be grouped by some keys.
For example, let's say I have a bunch of product with a name and price :
#+BEGIN_SRC haskell data Product = Product { name :: String, amount :: Double} #+END_SRC
I want to be able to group them by name and sum the amount. In SQL I would do
#+BEGIN_SRC sql SELECT name, SUM amount FROM products GROUP by name #+END_SRC
In haskell , I would like to be able to do something similar, which at some point involves collapsing all product with an identical name to one product. I could almost use a Monoid instance for Product, but I have a problem with aggregating the names. One solution would be to have, name being a =First String= instead of =String=, or maybe =Last String= or even maybe just ignore it and use =Const () String=. The traditional answer to this is, just define
#+BEGIN_SRC haskell data Product f = Product { name :: f String, amount :: Double} #+END_SRC
and then I can define a monoid instance for =Monoid (f String) => Monoid (Product f)=. Again, I might not be able to modify =Product= as it might be generated from a db schema. I can then have a =Product= (parametric) and a =DbProduct= non-parametric, but there we go. I need converters between them. Better generate =Product= from =DbProduct= and the required converters.
It might be because I'm not thinking and modelling the haskell way, and should realize there are code smells, which I should sort out. However, I often found that the solution to my problems could be easily solved by just copy pasting an existing type, add or modify a few fields and write a converter between the old and the new type. But I like DRY code and don't copy paste, so the answer is either TH or Generics. Generics, can probably take care of the converter but it can't generate the new data types, so I'll have to go the TH way.
- The applicative conversion context In order to have the generator the less smart as possible, every conversion are done within an applicative context. We use the =ConvertA a f b= type class which basically means that =a= can be converted into =b= in a =f= context. The main purpose of =ConvertA= is to lift normal value to applicative using =pure= if necessary but also convert between different functor when possible. =f= is usually either =Identity= (we know the conversion always success), =Maybe= (conversion can fail), =[]= or even =ZipList=.
This allows every converters to be written with the following shapes and compiles even if types mismatch. For example, given
#+BEGIN_SRC haskell data A a = A a data B b = B b (Maybe Int) #+END_SRC
The converter will be
#+BEGIN_SRC haskell aAtoB (A a) = B <$> convertA a <*> convertA () #+END_SRC
=()= will be converted to =Nothing= (needed for =Maybe Int=) and depending and =a= and =b=, we could convert between =A= and =B= or not. We could convert between =A Int= and =B Int= or =A (Maybe Int)= to =B ([Int])= using =Identity= (=Maybe Int= can be converted without loss to a =[Int=). However converting between =A (Maybe Int)= and =B Int= would require using =Maybe=. Converting between =A Double= and =B Int= wouldn't compiles. However, the compilation will fail not at the converter declaration, but when trying to use it.
- Examples For complete examples, with how to defines type transformation as well as how to use converted with different applicatives, the best is probably to look at the [[test/ExampleSpec.hs][example]] spec file.