Surgery for generic data types.
Transform data types before passing them to generic functions.
Surgery for generic data types
Modify, add, or remove constructors and fields in generic types, to be used with generic implementations.
Example
Here is a simple record type equipped with a checksum
function:
data Foo = Foo { x, y, z :: Int }
deriving (Eq, Generic, Show)
checksum :: Foo -> Checksum
Let's encode it as a JSON object with an extra "checksum"
key, looking like this, where X
, Y
, Z
are integers:
{ "x": X
, "y": Y
, "z": Z
, "checksum": X + Y + Z
}
We use genericParseJSON
/genericToJSON
to convert between JSON values and a generic 4-field record, and removeRField
/insertRField
to convert between that generic 4-field record and the 3-field Foo
.
Remove field
When decoding, we check the checksum and then throw it away.
instance FromJSON Foo where
parseJSON v = do
r <- genericParseJSON defaultOptions v
-- r: a generic 4-field record {x,y,z,checksum} (checksum at index 3).
let (cs, f) = (fmap fromOR . removeRField @"checksum" @3 . toOR') r
-- removeRField @"checksum" @3: split out the checksum field
-- from the three other fields. (cs, f) :: (Checksum, Foo)
if checksum f == cs then
pure f
else
fail "Checksum failed"
Insert field
When encoding, we must compute the checksum to write it out. We put the checksum in a pair (checksum f, f)
with the original record, and insertRField
can then wrap it into a 4-field record passed into genericToJSON
.
instance ToJSON Foo where
toJSON f =
(genericToJSON defaultOptions . fromOR' . insertRField @"checksum" @3 . fmap toOR)
(checksum f, f)
See also
Surgery for data types, introductory blog post with another example.
The
examples/
directory in the source repo.