maxigit / Metamorphosis

TemplateHaskell functions to generate types and converter function.
BSD 3-Clause "New" or "Revised" License
18 stars 3 forks source link

+BEGIN_SRC haskell

data AB = AB Int String

+END_SRC

to

+BEGIN_SRC haskell

data A = A Int
data B = B String

+END_SRC

+BEGIN_SRC haskell

data A = A Int
data B = B String

+END_SRC

to

+BEGIN_SRC haskell

data AB = AB Int String

+END_SRC

+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

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

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.

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.