Skip to content

Latest commit

 

History

History
136 lines (101 loc) · 5.4 KB

README.org

File metadata and controls

136 lines (101 loc) · 5.4 KB

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
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 …

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 :

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.

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

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.

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 example spec file.