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

Superior string spaghetti with PureScript-Jajanmen

Superior string spaghetti with PureScript-Jajanmen

Talk given for Haskell ITA in Milan about parsing type-level strings in PureScript to have well typed query parameters.

Justin Woo

July 01, 2018
Tweet

More Decks by Justin Woo

Other Decks in Programming

Transcript

  1. Superior string spaghetti
    with PureScript-Jajanmen
    1 Jul 18 @ Haskell ITA Milan

    View Slide

  2. Problem: Untyped Parameterized SQL Queries
    ● We want to work with parameterized SQL queries
    ○ But they’re always untyped
    queryDB :: forall params. String -> Record params -> Aff Foreign
    aff = queryDB
    "select name, count from mytable where name = $name and count = $count"
    { "$name": "Bill", "$count": 10 }
    ● But we know statically what the query string is here!
    ● What if we could use it at the type level and extract a type from it?

    View Slide

  3. ● In PureScript these are Symbol-kinded types (as opposed to Type-kinded)
    ○ Meaning that they have no value representation
    ○ “Carried” around in compile-time using string proxies
    data SProxy (sym :: Symbol) = SProxy
    ● You can reflect these to a value whenever you need it
    class IsSymbol (sym :: Symbol) where
    reflectSymbol :: SProxy sym -> String
    What is a “type level string”?

    View Slide

  4. How do we work with these?
    ● You can work with them at the type level with constraints
    symbolAppend :: forall left right appended.
    Symbol.Append left right appended => IsSymbol appended =>
    SProxy left -> SProxy right -> String
    symbolAppend _ _ = reflectSymbol (SProxy :: SProxy appended)
    ● The “appended” here is determined by the left and right by the functional
    dependencies in Symbol.Append

    View Slide

  5. What are functional dependencies?
    ● Mainly a way of stating which parameters of a type class determine the
    others
    ● Think Sum:
    ○ A + B = C
    ○ A and B determine C
    ○ C and B determine A
    ○ C and A determine B
    class Append (left :: Symbol) (right :: Symbol) (appended :: Symbol)
    | left right -> appended
    , right appended -> left
    , appended left -> right

    View Slide

  6. What if you could Cons Symbol like Lists?
    ● In PureScript 0.12.0, you can!
    ● “ABC” -> “A” “BC”
    class Cons (head :: Symbol) (tail :: Symbol) (symbol :: Symbol)
    | head tail -> symbol, symbol -> head tail
    ● This basically lets us parse Symbols!
    class ParseParamName (x :: Symbol) (xs :: Symbol)
    (acc :: Symbol) (out :: Symbol)
    | x xs -> acc out

    View Slide

  7. The problem with normal matching
    ● When parsing, we need to work with both the head and the tail
    ● E.g. when parsing a param name until a space or end of the string
    class ParseParamName (x :: Symbol) (xs :: Symbol)
    (acc :: Symbol) (out :: Symbol) | -- ...
    -- invalid overloading instances!
    instance endRParseParamName :: ( Symbol.Append acc x out ) =>
    ParseParamName x "" acc out
    instance spaceParseParamName :: ParseParamName " " xs out out
    ● X and XS are overlapping

    View Slide

  8. Instance chains at work
    ● We can specify in which order these overlapping instances should match
    ● First come, first served
    ● Uses functional dependencies to match instances (not constraints)
    instance endRParseParamName ::
    ( Symbol.Append acc x out
    ) => ParseParamName x "" acc out
    else instance spaceParseParamName ::
    ParseParamName " " xs out out

    View Slide

  9. How do these solve our problem?
    ● Now we have a complete picture of how to parse Symbols
    ● We can use this to extract specific labels to construct a record type for the
    parameters in our queries
    ● Hence, PureScript-Jajanmen
    getEm :: forall a b. AllowedParamType a => AllowedParamType b =>
    DBConnection -> { "$name" :: a, "$count" :: b } -> Aff Foreign
    getEm db = J.queryDB db queryP
    where
    queryP = SProxy :: SProxy
    "select name, count from mytable where name = $name and count = $count"

    View Slide

  10. Why Jajanmen?
    Spaghetti
    Plain, Easily ruined by Americans
    Stringly typed spaghetti code
    Jajanmen
    Delicious, not yet ruined by Americans
    Strongly typed inferred record types

    View Slide

  11. “But we could use QuasiQuotes”
    Mà, ho portato dei Tipi per gli
    spaghetti

    View Slide

  12. Top level function
    ● Jajanmen has one top level function:
    queryDB :: forall query params.
    IsSymbol query => ExtractParams query params =>
    SQLite3.DBConnection -> SProxy query -> { | params } -> Aff Foreign
    queryDB db queryP params =
    SQLite3.queryObjectDB db query params
    where query = reflectSymbol queryP
    ● ExtractParams extracts the type of params from our query string

    View Slide

  13. ExtractParams
    ● Extract params is a class with a single instance to start our parsing
    class ExtractParams (query :: Symbol) (params :: # Type)
    | query -> params
    instance extractParams ::
    ( Symbol.Cons x xs query
    , ExtractParamsParse x xs params
    ) => ExtractParams query params

    View Slide

  14. ExtractParamsParse
    ● Determines what to do based on the head and tail
    class ExtractParamsParse (x :: Symbol) (xs :: Symbol) (params :: # Type)
    | x xs -> params
    ● If the tail is empty, we know there are no row types to synthesize:
    instance endExtractParamsParse :: ExtractParamsParse x "" ()

    View Slide

  15. ● On “$”, parse out the parameter
    name and add it to our record
    else instance paramExtractParams ::
    ( Symbol.Cons y ys xs
    , ParseParamName y ys "$" out
    , Symbol.Cons z zs ys
    , Row.Cons out ty row' row
    , AllowedParamType ty
    , ExtractParamsParse z zs row'
    ) => ExtractParamsParse "$" xs row
    ExtractParamsParse (cont.)
    ● Otherwise, we can continue
    else instance nExtractParams ::
    ( Symbol.Cons y ys xs
    , ExtractParamsParse y ys row
    ) => ExtractParamsParse x xs row
    ● What is Row.Cons?
    ● What is AllowedParamType?

    View Slide

  16. Synthesizing row types with RowCons
    ● Remember that record types are parameterized by row type
    data Record :: # Type -> Type
    type MyRecord = { a :: Int, b :: String }
    ~ Record (a :: Int, b :: String )
    ● So it makes sense we can use RowCons to add to it
    class Cons (label :: Symbol) (a :: Type)
    (tail :: # Type) (row :: # Type)
    | label a tail -> row, label row -> a tail

    View Slide

  17. RowCons in a nutshell

    View Slide

  18. AllowedParamType
    ● Simple class to define which types are allowed to be parameters
    class AllowedParamType ty
    instance stringAllowedParamType :: AllowedParamType String
    instance intAllowedParamType :: AllowedParamType Int
    instance numberAllowedParamType :: AllowedParamType Number

    View Slide

  19. ParseParamName
    ● Used by the ExtractParamsParse instance for $
    class ParseParamName (x :: Symbol) (xs :: Symbol)
    (acc :: Symbol) (out :: Symbol)
    | x xs -> acc out
    ● Multiple stop conditions, with a general case for accumulating the
    parameter name:
    else instance nParseParamName ::
    ( Symbol.Cons y ys xs, Symbol.Append acc x acc'
    , ParseParamName y ys acc' out ) => ParseParamName x xs acc out

    View Slide

  20. ParseParamName instances
    x (head) xs (tail) acc (accumulate) out (param name)
    “)” “” (end) acc acc
    x “” (end) acc Append acc x
    “ “ xs acc acc
    “)” xs acc acc
    ● Covers usages in tests:

    View Slide

  21. Back to the top
    ● Hopefully now this all makes sense:
    queryDB :: forall query params.
    IsSymbol query => ExtractParams query params =>
    SQLite3.DBConnection -> SProxy query -> { | params } -> Aff Foreign
    getEm :: forall a b. AllowedParamType a => AllowedParamType b =>
    DBConnection -> { "$name" :: a, "$count" :: b } -> Aff Foreign
    getEm db = J.queryDB db queryP
    where
    queryP = SProxy :: SProxy
    "select name, count from mytable where name = $name and count = $count"

    View Slide

  22. Conclusion
    ● With PureScript 0.12, we can…
    ○ Extract a lot of information from Symbols
    ○ Use instance chains to safely use overlapping instances
    ○ Use existing techniques from 0.11.x to synthesize types using the extracted information
    from Symbols
    ● Hopefully this gives you some ideas of what you might try tackling next

    View Slide

  23. Thanks!
    ● More detailed post here
    ○ https://github.com/justinwoo/my-blog-posts#well-typed-parameterized-sqlite-parameters
    -with-purescript
    ● Csongor Kiss’s post on his printf library: http://kcsongor.github.io/purescript-safe-printf/
    ● PureScript 0.12.0 release notes
    ○ https://github.com/purescript/purescript/releases/tag/v0.12.0
    ● Rough getting started with PureScript guide
    ○ https://github.com/justinwoo/my-blog-posts#setting-up-purescript-in-march-2018
    ● Also see managing package sets
    ○ https://github.com/justinwoo/my-blog-posts#managing-psc-package-sets-with-dhall

    View Slide