Quantcast
Channel: Silk Engineering Blog
Viewing all articles
Browse latest Browse all 32

Writing a generic XML pickler

$
0
0

In a previous post, Sebas explained that we use so-called XML picklers to convert Haskell data types to XML. Since these picklers have a regular structure, we don’t write them by hand, but derive them automatically using generic programming techniques. In this post, I’ll explain how our generic XML pickler works. The code shown here has been made available on hackage. We use the regular library to represent data types generically. It allows you to build a structure for your type that is isomorphic to your type, but built up from standard building blocks, called functors. This is possible because Haskell data types have a standard structure: a choice of one of several constructors (this is often called a sum), each having a number of fields (this is often called a product).

As an example, we will represent a simple user data type:

data User = User
         { name  :: String
         , email :: String
         , admin :: Bool
         }

The generic representation of this type is called a pattern functor. For the user data type, it will look like this:

type instance PF User = C User_User_
 (   S User_User_name_  (K String)
 :*: S User_User_email_ (K String)
 :*: S User_User_admin  (K Bool)
 )

Here we see a few of the functors that are the building blocks of our generic representation: The C type marks constructors, the S type marks record labels, K marks the types that make up the fields of our record, and (:*:) is the product mentioned earlier, and is very similar to the (,) used for constructing tuples. There are three more functors in regular: I, which marks recursive positions in a type; (:+:), which sums together different constructors; and U, for constructors without fields.

To write a generic function, we define a type class which contains this generic function, and give an instance for each of these functors. If we then also define conversion functions from our data type to the generic representation and back, we can apply the function to our data type. Note that for regular, these conversion functions, and the representation given above, can all be generated using Template Haskell.

Since we want to write a generic XML pickler for use with the HXT library, we’ll define a type class containing such a pickler:

class GXmlPickler f where
 gxpicklef :: PU a -> PU (f a)

This says that we can give a generic pickler for one of the functors f, containing a’s at the recursive positions, if we have a pickler for a’s. I’ll now show the instances for all the functors, and explain them one by one.

The first is the instance for I, marking recursive positions. Since we get a pickler for the resursive positions, all we have to do is wrap it in the I constructor when unpickling, and remove that constructor when pickling. HXT provides the xpWrap function to transform a pickler like this.

instance GXmlPickler I where
 gxpicklef = xpWrap (I, unI)

Next, we’ll tackle K. Here, we require that the type of the field has its own pickler, and use that. We again use xpWrap to add and remove the constructor of the functor. We provide a special instance for String, since it doesn’t have a standard pickler. We choose to use the pickler that allows empty strings.

instance XmlPickler a => GXmlPickler (K a) where
 gxpicklef _ = (K, unK) `xpWrap` xpickle

instance GXmlPickler (K String) where
 gxpicklef _ = (K, unK) `xpWrap` xpText0

U works similarly. We use the pickler for (), and use xpWrap to go from a () to a U and back.

instance GXmlPickler U where
 gxpicklef _ = (const U, const ()) `xpWrap` xpUnit

The case for (:+:) is interesting. Remember that (:+:) represents a choice between two functors. If we have picklers for both of these, we can define a pickler for the sum as follows: during conversion to XML, we can pattern match on L or R (the constructors of (:+:)) and choose the left or right pickler appropriately. During conversion from XML, we try the first pickler. If it fails (an unpickler returns a Maybe value) we try the second. Since HXT doesn’t seem to have a combinator that follows this logic, let’s define one ourselves:

xpEither :: PU (f r) -> PU (g r) -> PU ((f :+: g) r)
xpEither (PU fl tl sa) (PU fr tr sb) = PU
 (\(x, st) -> case x of
                L y -> fl (y, st)
                R y -> fr (y, st))
 (\x -> case tl x of
          (Nothing, _) -> lmap (fmap R) (tr x)
          r            -> lmap (fmap L) r)
 (sa `scAlt` sb)
 where lmap f (a, b) = (f a, b)

Note that we take apart the PU type, and create a pretty printer, a parser and a schema separately. We can use this function in the GXmlPickler instance by supplying it with two picklers, created by recursive calls to gxpicklef.

instance (GXmlPickler f, GXmlPickler g) => GXmlPickler (f :+: g) where
 gxpicklef f = xpEither (gxpicklef f) (gxpicklef f)

The instance for (::) is easy again. Since (::) combines two functors, we can require that these two have a pickler. We use xpPair to combine these into a pickler for a pair, and then use xpWrap again to convert it into a pickler for (:*:).

instance (GXmlPickler f, GXmlPickler g) => GXmlPickler (f :*: g) where
 gxpicklef f = (uncurry (:*:), \(a :*: b) -> (a, b))
               `xpWrap`
               (gxpicklef f `xpPair` gxpicklef f)

Note that so far, we haven’t created any XML tags ourselves. We’ve only combined picklers. Now we come to the instances for constructors and record selectors. Here, we’ll use the constructor or selector name (converted to lowercase) to generate and parse xml tags. This is done with the xpElem combinator.

instance (Constructor c, GXmlPickler f) => GXmlPickler (C c f) where
 gxpicklef f = xpElem (map toLower $ conName (undefined :: C c f r))
                 ((C, unC) `xpWrap` gxpicklef f)

instance (Selector s, GXmlPickler f) => GXmlPickler (S s f) where
 gxpicklef f = xpElem (map toLower $ selName (undefined :: S s f r))
                 ((S, unS) `xpWrap` gxpicklef f)

We now have picklers for all generic representations built from the standard functors in regular. We still need a top level function that works on our real data types, though. Regular provides the two functions to and from to convert between data types and their generic representation. We can use these together with xpWrap to change our generic pickler into a pickler for real data types. Another matter we have not addressed is the first argument to gpicklef, which is a pickler for the recursive positions. Here we pass the top level function, since the recursive position contains our original data type again.

gxpickle :: (Regular a, GXmlPickler (PF a)) => PU a
gxpickle = (to, from) `xpWrap` gxpicklef gxpickle

And that’s all for the generic XML pickler. Using it is very simple. To use it for our user data type above, we import Generic.Regular, Generic.Regular.TH, Generic.Regular.XmlPickler and Text.XML.HXT.Arrow.Pickle. We can then derive the Regular instance for user (note that we need a little bit of extra code, since Template Haskell cannot generate type family instances yet), and make an XmlPickler instance:

$(deriveAll ''User "PFUser")
type instance PF User = PFUser

instance XmlPickler User where
 xpickle = gxpickle

And that’s it! The package is available as regular-xmlpickler on hackage, and the source can also be found on github. The packages regular and HXT can also be found on hackage.


Viewing all articles
Browse latest Browse all 32

Trending Articles