Emulating Go’s struct field tags in Haskell (I)
The Go language has a nice feature: when declaring a struct, you can assign metadata to each field. That metadata is later avaliable using reflection, and for example it can be used to determine the field’s external name when serializing to JSON .
In Haskell, aeson is by far the most popular JSON library. You can manually write parsers-decoders for your records, but there’s also a “don’t-make-me-work” option based on generics that derives the parser-decoder for you, using the names of your fields as the keys of the JSON object.
But I would argue that Go’s approach has advantages over aeson’s generic deriving. The reason is that in Go the names of the JSON keys are decoupled from the internal fields of the record. If you rename a record field during a refactoring your stored documents won’t suddenly fail to parse. Also, you can use reserved words as JSON keys without any problems.
Oh well, writing the parsers and decoders manually isn’t that hard anyway. However, if you want to write both a parser and a decoder you’ll have to refer to each external key twice, which is gross.
So what now? Should we seek some alternative to generics? Nah. Generics are like XML: if they aren’t working for you, you are not using them enough. Enter generics-sop.
generics-sop is a library that builds on top of the conventional generic machinery of Haskell and provides a more “structured” way to process the representation of a record (sop stands for “sums-of-products”, in the algebra of algebraic data types sense). It also lets you wrap each field of the representation in a type constructor and perform Applicative-like operations across all the fields. In that respect, it is similar to the Higher Kinded Data pattern, only you start with a perfectly vanilla record instead of wrapping the fields manually from the beginning.
To do anything, first we need to extract a type level list of type-level field names (of kind Symbol) from the record. This type-level metadata is obtained through the DatatypeInfoOf associated type family of the HasDatatypeInfo typeclass, which you must define for your record, like this:
DatatypeInfoOf is a bit like a function at the type level: you apply it to the type of your record like DatatypeInfoOf MyRecord and the result is another type of kind DatatypeInfo that encodes the metadata.
The kind of the result is DatatypeInfo, but the returned type might be the lifted constructor ADT or the lifted constructor Newtype. Here we are only interested in the former, in fact we want to give a compilation error for the latter.
To handle all this pattern-matching at the type level, and to extract the field names from the whole metadata, we define the following type families:
We employ user-defined type errors to hopefully give the users an easier time when they try to apply the type families on an unsupported type.
Ok, now we have a FieldNamesOf type family that produces a type-level list of field names from our record types. The next step is to associate each element of that type-level list with an external field name. We’ll leave that for a later post.
The actual code can be found here.