Slide 1

Slide 1 text

Well made Tortellini with PureScript 24 Apr @NY PureScript by JW

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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?

Slide 4

Slide 4 text

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)

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

Docs to Types [section1] fruit=apple isRed=true seeds=4 [MOOMINJUMALA] children=banana,grape,kiwi [麻婆豆腐] type TestIni = { section1 :: { fruit :: String , isRed :: Boolean , seeds :: Int } , "MOOMINJUMALA" :: { children :: Array String } , "麻婆豆腐" :: {} }

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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 }

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

● Iterates a RowList and gives back a Record.Builder class ReadDocumentSections (xs :: RowList) (from :: # Type) (to :: # Type) | xs -> from to where readDocumentSections :: RLProxy xs -> StrMap (StrMap String) -> Except UhOhSpaghettios ( Builder (Record from) (Record to)) ReadDocumentSections

Slide 12

Slide 12 text

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 }

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

Compose and Build addY :: Builder {x :: Int} {x :: Int, y :: Int} addY = Builder.insert (SProxy :: SProxy "y") 2 addZ :: Builder {x :: Int, y :: Int} {x :: Int, y :: Int, z :: Int} addZ = Builder.insert (SProxy :: SProxy "z") 3 addYZ :: Builder {x :: Int} {x :: Int, y :: Int, z :: Int} addYZ = addZ <<< addY xyz :: {x :: Int, y :: Int, z :: Int} xyz = Builder.build addYZ {x : 1}

Slide 15

Slide 15 text

● Iterates a RowList and gives back a Record.Builder class ReadDocumentSections (xs :: RowList) (from :: # Type) (to :: # Type) | xs -> from to where readDocumentSections :: RLProxy xs -> StrMap (StrMap String) -> Except UhOhSpaghettios ( Builder (Record from) (Record to)) ReadDocumentSections

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

from’, to

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

ReadSection ● Largely same as ReadDocument class ReadSection (xs :: RowList) (from :: # Type) (to :: # Type) | xs -> from to where readSection :: RLProxy xs -> StrMap String -> Except UhOhSpaghettios (Builder (Record from) (Record to)) ● But Cons instance uses ReadIniField class ReadIniField a where readIniField :: String -> Except UhOhSpaghettios a

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

parsellIni parsellIni :: forall rl row . RowToList row rl => ReadDocumentSections rl () row => String -> Either UhOhSpaghettios { | row } parsellIni s = do doc <- lmap (pure <<< ErrorInParsing) $ parsellIniDocument s builder <- runExcept $ readDocumentSections (RLProxy :: RLProxy rl) doc pure $ Builder.build builder {} That’s it!

Slide 24

Slide 24 text

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"]

Slide 25

Slide 25 text

Docs to Types [section1] fruit=apple isRed=true seeds=4 [MOOMINJUMALA] children=banana,grape,kiwi [麻婆豆腐] type TestIni = { section1 :: { fruit :: String , isRed :: Boolean , seeds :: Int } , "MOOMINJUMALA" :: { children :: Array String } , "麻婆豆腐" :: {} }

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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