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)
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?
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
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
• 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
a library to do this • Provide data types deriving generics -> done! parseIni :: Generic record => ReadDocumentSections (Rep record) => Text -> Either UhOhSpaghettios record
| ErrorAtDocumentProperty Text UhOhSpaghetto | ErrorAtSectionProperty Text UhOhSpaghetto | ErrorInParsing Text +-- What's the plural of UhOhSpaghetto? type UhOhSpaghettios = NonEmpty UhOhSpaghetto
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 :: * -> *
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
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
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))
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
Text -> Either UhOhSpaghettios record parseIni s = do doc <- first (pure . ErrorInParsing . T.pack) $ parseIniDocument s runExcept $ to <$> readDocumentSections doc
• 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!