Who am I ● Korean-American living in Helsinki ○ I didn’t know what Moomin was before I moved there ● Used to live in DC (Clarendon) ○ This is where you’re supposed to boo ● PureScript user since 2016 ○ Now writing PureScript on AWS Lambda ● More known for my shitty memes nowadays ● Also known for throwing RowToList at everything
Related Haskell talk ● Similar talk given earlier this month at Helsinki Haskell ● Uses GHC Generics to accomplish something similar ● Requires defined types everywhere, no row types Is programming without RowToList even programming at all?
Problem ● We want to work with INI files ● INI files are documents of sections of fields [header] key=value ● Dio mio, people model this as StrMap (StrMap String)
What do we want ● We know exactly which sections we want to read from in a document ● We know exactly which fields we want to read from in a section ● We want a record of records! ○ And better yet, we want a record alias of record aliases! type Document = { section :: { key :: value } }
Naive approach ● We can try to write out concrete functions readMyField :: String -> Either Error MyType readMySection :: StrMap String -> Either Error MySection readMyDocument :: StrMap (StrMap String) -> Either Error MyDocument ● But being error-prone and boring, why would we? ● We can associate section/field names directly to the labels in our record ○ FAQ: Need to rename? See Record.rename
How? ● First we run INI document text body through a parser parsellIniDocument :: String -> Either ParseError IniDocument ● Then we use the row type information to do stuff readDocumentSections :: RLProxy xs -- from RowToList row xs -> StrMap (StrMap String) -> Except _ _
Tortell[ini] - aka Wontons made by Italians ● I wrote a library to do this ● Use the parsellIni function with a type annotation or concrete context, done! parsellIni :: forall rl row . RowToList row rl => ReadDocumentSections rl () row => String -> Either UhOhSpaghettios { | row }
Mikä on “UhOhSpaghettios”? ● We should handle errors up front data UhOhSpaghetto = Error String | ErrorAtDocumentProperty String UhOhSpaghetto | ErrorAtSectionProperty String UhOhSpaghetto | ErrorInParsing ParseError ● What is the plural of UhOhSpaghetto? type UhOhSpaghettios = NonEmptyList UhOhSpaghetto
Wait, what is Record.Builder? ● Why keep clumsily making clones? newClonesEveryTime = let a = { x: 1 } b = { x: a.x, y: 2 } in Record.insert (SProxy :: SProxy "z") 3 b ● What do we know we want? { x :: Int } -> { x :: Int, y :: Int, z :: Int }
Builder! ● We want to go from { x :: Int } to { x :: Int, y :: Int, z :: Int } ● We have two insertion operations ● Why can’t we compose them? ○ Properly like Semigroupoids, not “compose” in some vague sense ● You can! compose :: forall f a b c. Semigroupoid f => f b c -> f a b -> f a c composeBuilderAlias :: forall a b c. Builder b c -> Builder a b -> Builder a c
nilReadDocumentSections ● At Nil, we no longer have any more fields to build ● Since Record.Build is a Category, we can return id ○ i.e. building an empty record is done by applying id to an empty record instance nilReadDocumentSections :: ReadDocumentSections Nil () () where readDocumentSections _ _ = pure id
consReadDocumentSections instance consReadDocumentSections :: ( IsSymbol name , RowToList inner xs , ReadSection xs () inner , RowCons name { | inner } from' to , RowLacks name from' , ReadDocumentSections tail from from' ) => ReadDocumentSections (Cons name { | inner } tail) from to where Section is a a pair of name and type { | inner } Then we can get a builder for the fields The section should be in the document The section should be missing from the subtype from’ from the rest of the sections The rest of the document should be read for sections
consReadDocumentSections body readDocumentSections _ sm = do case SM.lookup name sm of Nothing -> throwError <<< pure <<< ErrorAtDocumentProperty name <<< Error $ "Missing section in document" Just section -> do builder <- withExcept' $ readSection (RLProxy :: RLProxy xs) section let value = Builder.build builder {} rest <- readDocumentSections (RLProxy :: RLProxy tail) sm let first :: Builder (Record from') (Record to) first = Builder.insert nameP value pure $ first <<< rest where nameP = SProxy :: SProxy name name = reflectSymbol nameP withExcept' = withExcept <<< map $ ErrorAtDocumentProperty name
ReadIniField instances ● Similar to Simple-JSON ReadForeign instance intReadIniField :: ReadIniField Int where readIniField s = maybe (throwError <<< pure <<< Error $ "Expected Int, got " <> s) pure $ fromNumber $ readInt 10 s instance arrayReadIniField :: ( ReadIniField a ) => ReadIniField (Array a) where readIniField s = traverse readIniField $ split (Pattern ",") s
This all looks a bit familiar Surprise, you now know how Simple-JSON works! (Also every other RowToList demo I have) class ReadForeignFields (xs :: RowList) (from :: # Type) (to :: # Type) | xs -> from to where getFields :: RLProxy xs -> Foreign -> F (Builder (Record from) (Record to)) instance readFieldsCons :: ( IsSymbol name , ReadForeign ty , ReadForeignFields tail from from' , RowLacks name from' , RowCons name ty from' to ) => ReadForeignFields (Cons name ty tail) from to where
Usage ● We can use the testDoc string and TestIni type from earlier suite "parsellIni" do test "works" do case parsellIni testDoc of Left e -> failure $ show e Right (result :: TestIni) -> do equal result .section1.fruit "apple" equal result .section1.isRed true equal result .section1.seeds 4 equal result ."MOOMINJUMALA".children ["banana","grape","pineapple"]
Didn’t I say only context is required? ● Yep, you really only need an annotation or a concretely typed function to set the type by context test "works2" do case parsellIni testDoc of Left e -> failure $ show e Right (result :: {section1 :: {fruit :: String}}) -> do equal result .section1.fruit "apple" test "works3" do let equal' :: {section1 :: {fruit :: String}} -> _ equal' r = equal r.section1.fruit "apple" either (failure <<< show) equal' $ parsellIni testDoc
Back to the Haskell version ● Can’t make record type aliases ○ Records are product types with metaselectors ● We have to define data types for the whole document and each section instead of being able to define nested records ● Which leads us to use GHC Generics to work generically ○ But without translation to another structure, no coercion between structurally similar (position dependent) ○ Check out Generics-Lens by Csongor if interested in ways to work with records in PureScripty ways
Row types make the difference! Haskell: data Config = Config { section1 :: Section1 } deriving Generic data Section1 = Section1 { apple :: Text } deriving Generic to/from GHC Generics PureScript type Config = { section1 :: { apple :: String } } Record subtypes, Builder, etc HELVETIN HYVÄÄ ei jumalauta
Thanks! ● Blog post about both PureScript and the Haskell versions github.com/justinwoo/my-blog-posts#dec-28-2017 ● Repo github.com/justinwoo/purescript-tortellini/ ● Twitter @jusrin00 ● More on how you can use row type inference for stuff speakerdeck.com/justinwoo/easy-json-deserialization-with-simple-json-an d-record