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
data AB = AB Int String
to
data A = A Int
data B = B String
- merge some types into a bigger one
data A = A Int
data B = B String
to
data AB = AB Int String
- Changes the type of some fields
data Product = Product { name :: String, price :: Double }
to
data ProductM = ProductM { name :: String, price :: (Maybe Double) }
- lift plain types to parametric ones
data Product = Product { name :: String, price :: Double }
to
data ProductF f = ProductF { name :: String, price :: f Double }
etc …
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 :
data Product = Product { name :: String, amount :: Double}
I want to be able to group them by name and sum the amount. In SQL I would do
SELECT name, SUM amount
FROM products
GROUP by name
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
data Product f = Product { name :: f String, amount :: Double}
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.
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
data A a = A a
data B b = B b (Maybe Int)
The converter will be
aAtoB (A a) = B <$> convertA a <*> convertA ()
()
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.
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 example spec file.