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
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?
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 } }
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
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 _ _
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 }
data UhOhSpaghetto = Error String | ErrorAtDocumentProperty String UhOhSpaghetto | ErrorAtSectionProperty String UhOhSpaghetto | ErrorInParsing ParseError • What is the plural of UhOhSpaghetto? type UhOhSpaghettios = NonEmptyList UhOhSpaghetto
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 }
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
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
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
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 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
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
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"]
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
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
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