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

Well made Tortellini with PureScript

Well made Tortellini with PureScript

Talk on PureScript-Tortellini for NY PureScript, 24 Apr 2018

Justin Woo

April 24, 2018
Tweet

More Decks by Justin Woo

Other Decks in Programming

Transcript

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

    View Slide

  2. 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

    View Slide

  3. 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?

    View Slide

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

    View Slide

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

    View Slide

  6. 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

    View Slide

  7. 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 }
    , "麻婆豆腐" :: {} }

    View Slide

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

    View Slide

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

    View Slide

  10. 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

    View Slide

  11. ● 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

    View Slide

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

    View Slide

  13. 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

    View Slide

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

    View Slide

  15. ● 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

    View Slide

  16. 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

    View Slide

  17. 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

    View Slide

  18. from’, to

    View Slide

  19. 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

    View Slide

  20. 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

    View Slide

  21. 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

    View Slide

  22. 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

    View Slide

  23. 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!

    View Slide

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

    View Slide

  25. 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 }
    , "麻婆豆腐" :: {} }

    View Slide

  26. 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

    View Slide

  27. 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

    View Slide

  28. 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

    View Slide

  29. 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

    View Slide