Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Preparing TortellINI with GHC Generics

Preparing TortellINI with GHC Generics

This talk was given at Helsinki Haskell, 10 Apr 2018

Justin Woo

April 10, 2018
Tweet

More Decks by Justin Woo

Other Decks in Programming

Transcript

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

    View Slide

  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)

    View Slide

  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?

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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))))

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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 :: * -> *

    View Slide

  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.

    View Slide

  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

    View Slide

  14. Product instance is also just unwrapping
    instance
    ( ReadDocumentSections a
    , ReadDocumentSections b
    ) => ReadDocumentSections (a :*: b) where
    readDocumentSections hm =
    (:*:)
    <$> readDocumentSections @a hm
    <*> readDocumentSections @b hm

    View Slide

  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

    View Slide

  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

    View Slide

  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))

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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!

    View Slide

  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

    View Slide