Preparing TortellINI with GHC Generics

Preparing TortellINI with GHC Generics

This talk was given at Helsinki Haskell, 10 Apr 2018

3b48c91bf6b6f0bfd0fda50625598656?s=128

Justin Woo

April 10, 2018
Tweet

Transcript

  1. Preparing TortellINI with GHC Generics Justin Woo Haskell Helsinki 10

    Apr 2018
  2. Problem • INI files need to be read in to

    our programs: [header] key=value • Generally, we know what we want from this statically • Porca Madonna, existing approaches give us only HashMap Text (HashMap Text Text)
  3. • We know statically what the top-level Dict is as

    individual Sections of Header and Contents • We want a Record of Records! data MyDocument = MyDocument { mySection :: MySection } data MySection = MySection { myField :: MyType } What do we actually want?
  4. Naive approach • Write manually out some functions readMyDocument ::

    HashMap Text (HashMap Text Text) -> Either Error MyDocument readMySection :: HashMap Text Text -> Either Error MySection readMyField :: Text -> Either Error MyType • But we already know that we want a direct correspondence of section/field names to the HashMap! ◦ And we can always write a regular function to convert our INI model type to our actual domain type
  5. But how? • We can first parse INI into the

    nested hashmap for parsing the structure correctly parseIniDocument :: Text -> Either Error HashMap Text (HashMap Text Text) • Then we need to go through this nested hashmap and map its contents to our records
  6. How can we solve this problem… generically? • GHC Generics!

    • GHC can derive Generic for us, and we can write code that works with those generic representations class Generic a where type Rep a :: * -> * from :: a -> (Rep a) x to :: (Rep a) x -> a
  7. What does this look like? data MySectionType = MySection {

    apple :: Int, banana :: Bool, cherry :: Int } deriving Generic type instance Rep MySection = D1 ('MetaData "MySectionType" ...) (C1 ('MetaCons "MySection" ...) (S1 ('MetaSel ('Just "apple") ...) (Rec0 Int) :*: (S1 ('MetaSel ('Just "banana") ...) (Rec0 Bool) :*: S1 ('MetaSel ('Just "cherry") ...) (Rec0 Int))))
  8. What is D1, C1, S1, Rec0, K1? For your sanity:

    read docs from Generics.Deriving.Base rather than from GHC.Generics PureScript’s Generics-Rep make this easier to use with less information overload
  9. Tortell[ini] - aka Wontons made by Italians • I wrote

    a library to do this • Provide data types deriving generics -> done! parseIni :: Generic record => ReadDocumentSections (Rep record) => Text -> Either UhOhSpaghettios record
  10. What is Either UhOhSpaghettios record? data UhOhSpaghetto = Error Text

    | ErrorAtDocumentProperty Text UhOhSpaghetto | ErrorAtSectionProperty Text UhOhSpaghetto | ErrorInParsing Text +-- What's the plural of UhOhSpaghetto? type UhOhSpaghettios = NonEmpty UhOhSpaghetto
  11. What is ReadDocumentSections? class ReadDocumentSections (f :: * -> *)

    where readDocumentSections :: HashMap Text (HashMap Text Text) -- INI document -> Except UhOhSpaghettios (f a) Why kind * -> *? Remember that Rep a has the kind: type Rep a :: * -> *
  12. Instances of ReadDocumentSections Which do we need? 1. Datatype (to

    unwrap) 2. Single constructor (to unwrap) 3. Product (for the record and its fields) 4. Selector (for each record field) We don’t need Sum, etc.
  13. Unwrapping instances instance ReadDocumentSections a => ReadDocumentSections (D1 meta a)

    where readDocumentSections hm = M1 <$> readDocumentSections @a hm instance ReadDocumentSections a => ReadDocumentSections (C1 meta a) where readDocumentSections hm = M1 <$> readDocumentSections @a hm
  14. Product instance is also just unwrapping instance ( ReadDocumentSections a

    , ReadDocumentSections b ) => ReadDocumentSections (a :*: b) where readDocumentSections hm = (:*:) <$> readDocumentSections @a hm <*> readDocumentSections @b hm
  15. Selector instance head instance ( KnownSymbol name -- statically known

    Symbol, field name , Generic t -- type has a Generic instance , rep ~ Rep t -- convenience alias , ReadSection rep -- the representation can be used to read section ) => ReadDocumentSections (S1 ('MetaSel ('Just name) z x c) (K1 r t)) where
  16. Selector instance body readDocumentSections hm = case HM.lookup (T.toLower name)

    hm of -- lookup if name exists Nothing -> -- if not, we have a problem throwE . pure . ErrorAtDocumentProperty name . Error $ "Missing field in document " Just x -> do -- if we do, attempt reading section value <- withExcept' $ to <$> readSection @rep x pure $ M1 (K1 value) -- return the selector generic accordingly where name = T.pack $ symbolVal @name Proxy withExcept' = withExcept . fmap $ ErrorAtDocumentProperty name
  17. ReadSection • Pretty much the same as ReadDocument • But

    with two differences class ReadDocumentSections (f :: * -> *) where readDocumentSections :: (HashMap Text Text) -- INI section -> Except UhOhSpaghettios (f a) instance ( KnownSymbol name, ReadIniField t ) => ReadSection (S1 ('MetaSel ('Just name) z x c) ( K1 r t))
  18. ReadIniField class ReadIniField a where readIniField :: Text -> Except

    UhOhSpaghettios a instance ReadIniField Text where readIniField = pure instance ReadIniField Int where readIniField s = case AP.parseOnly AP.decimal s of Left e -> throwE (pure . Error . T.pack $ e) Right x -> pure x
  19. Altogether parseIni :: Generic record => ReadDocumentSections (Rep record) =>

    Text -> Either UhOhSpaghettios record parseIni s = do doc <- first (pure . ErrorInParsing . T.pack) $ parseIniDocument s runExcept $ to <$> readDocumentSections doc
  20. data Config = Config { section1 :: Section1 } deriving

    (Show, Eq, Generic) data Section1 = Section1 { apple :: Text } deriving (Show, Eq, Generic) testDoc = intercalate "\n" [ "[section1]" , "apple=banana" ] -- ... case parseIni testDoc of -- ... Right Config {section1 = Section1 {apple}} -> do apple `shouldBe` "banana" Usage
  21. That’s it! • GHC Generics usage/Datatype Generics in a nutshell

    • Concrete types, generic implementation • What “generics” should mean ◦ (and does mean in PLT circles) • Changes your thinking of problems in general ◦ Start to see parallels between term and type level programming ◦ Solve problems with first-class solutions instead of codegen ◦ Use constraints to give yourself stronger guarantees ◦ And more!
  22. Thanks! • Generics.Deriving docs: http://hackage.haskell.org/package/generic-deriving/docs/Generics-Derivi ng-Base.html • Code in Tortellini:

    https://github.com/justinwoo/tortellini • Related post: https://github.com/justinwoo/my-blog-posts#dec-28-2017