View on GitHub

idris2-sop

Idris port of Haskell's sop-core library

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 Monsters 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.