Metadata
In the last part of the tutorial, we experimented with automatically deriving different interface implementations. Those examples could be derived directly from a data type’s type-level code without the need for additional information.
For some interfaces like Show
, however, this is not enough: In order
to derive a Show
instance, we need to know about constructor names and
possibly record field names. In this part of the tutorial we learn how to
get access to those and how to make use of this information
to enhance our own Encoder
and Decoder
interfaces from the
last part.
module Docs.Metadata
import Generics.Derive
import Docs.Deriving
%language ElabReflection
Meta
: An Interface for Metadata
Getting access to a data type’s metadata is as simple as deriving
its Meta
interface (from module Generics.Meta
). This gives us access
to functions meta
and metaFor
, both of which return a TypeInfo
record, wrapping a product of ConInfo
values.
Like SOP
, TypeInfo
is indexed over a list of lists of values to
match the structure of the generic code of a data type.
Before we learn how to use metadata to write our own interface
implementations, here are two data types with automatically
derived Meta
and Show
implementations (we have to use fully
qualified names, because module Docs.Deriving
contains private data types
with identical names, which seems to confuse Idris):
export
data Spell = MkSpell Nat String
%runElab derive "Docs.Metadata.Spell" [Generic, Meta, Eq, Ord, DecEq, Show]
export
data Monster : Type where
Goblin : (hp : Int) -> (name : String) -> Monster
Demon : (hp : Int) -> (sp : Int) -> (spells : List Spell) -> Monster
Skeleton : (hp : Int) -> (armor : Int) -> Monster
%runElab derive "Docs.Metadata.Monster" [Generic, Meta, Eq, Ord, DecEq, Show]
An Encoder
for Sum Types
So far, we only supported the deriving of decoders for product types. We’d like to also support sum types, by prefixing encodings with the corresponding constructor’s name:
encodeCon : Encode (NP f ks) => ConInfo ks -> NP f ks -> List String
encodeCon ci np = ci.conName :: encode np
Since a type’s constructors are wrapped in an NP
parameterized
by the same type level list as its generic representation,
we can use the usual SOP combinators to generate an
encoding for a SOP
value. As can be guessed from the type of encodeCon
we can use hcliftA2
followed by hconcat
:
genEncode : Meta t code => (all : POP Encode code) => t -> List String
genEncode {all = MkPOP \_} = encodeSOP (metaFor t) . from
where encodeSOP : TypeInfo code -> SOP I code -> List String
encodeSOP (MkTypeInfo \_ \_ cons) =
hconcat . hcliftA2 (Encode . NP I) encodeCon cons . unSOP
The functions to be used in derive
are verbatim copies of the
ones used in the last post, but they call a different version
of genEncode
, therefore we have to include them here:
EncodeVis : Visibility -> DeriveUtil -> InterfaceImpl
EncodeVis vis g = MkInterfaceImpl "Encode" vis []
`(MkEncode genEncode)
(implementationType `(Encode) g)
Encode' : DeriveUtil -> InterfaceImpl
Encode' = EncodeVis Public
Decoding Sum Types: A Use Case for injections
We will need a new SOP technique for decoding sum types.
But first, decoding a single constructor seems to be straight
forward: We read the expected constructor name before
decoding the arguments.
However, as can be seen in the implementation of decodeCon
we somehow need to convert the decoded n-ary sum to a SOP
value by applying the correct sequence of S
and Z
constructors:
decodeCon : forall ks . Decode (NP f ks)
=> ConInfo ks -> (toSOP : NP f ks -> SOP f kss) -> Parser (SOP f kss)
decodeCon ci toSOP = string ci.conName \*> map toSOP decode
Function toSOP
is called an injection into the n-ary sum. Module Data.SOP
provides function injectionsSOP
, returning an n-ary product of all
injections from n-ary products into a sum of products parameterized over
the given typelevel list of lists. In order to combine the resulting
n-ary product of parsers, we use function hchoice
making use of
the Alternative
instance of Parser
:
-- Generic decoding function
genDecode : {code : \_} -> Meta t code => (all : POP Decode code) => Parser t
genDecode {all = MkPOP \_} = to <$> decodeSOP (metaFor t)
where decodeSOP : TypeInfo code -> Parser (SOP I code)
decodeSOP (MkTypeInfo \_ \_ cons) =
hchoice $ hcliftA2 (Decode . NP I) decodeCon cons injectionsSOP
Again, the functions to be used in derive
are verbatim copies of the
ones used in the last post:
DecodeVis : Visibility -> DeriveUtil -> InterfaceImpl
DecodeVis vis g = MkInterfaceImpl "Decode" vis []
`(MkDecode genDecode)
(implementationType `(Decode) g)
Decode' : DeriveUtil -> InterfaceImpl
Decode' = DecodeVis Public
We can now derive encoders and decoders for Monster
s and
test them at the REPL:
%runElab derive "Docs.Metadata.Spell" [Encode', Decode']
%runElab derive "Docs.Metadata.Monster" [Encode', Decode']
export
demon : Monster
demon = Demon { hp = 530
, sp = 120
, spells = [MkSpell 40 "Disintegrate", MkSpell 10 "Utterdark"]
}
export
encDemon : List String
encDemon = encode demon
export
decDemon : Either (List String) Monster
decDemon = parse decode encDemon
export
printDecDemon : IO ()
printDecDemon = printLn decDemon
Conclusion
Again, the SOP approach provides powerful abstraction to write generic interface implementations. This post completes the part about deriving interface implementations. Still, there are other possibilities and techniques of this library to explore, for instance the ability to provide automatically generated lenses.