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. 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?
  2. • 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”?
  3. 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
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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"
  9. Why Jajanmen? Spaghetti Plain, Easily ruined by Americans Stringly typed

    spaghetti code Jajanmen Delicious, not yet ruined by Americans Strongly typed inferred record types
  10. 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
  11. 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
  12. 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 "" ()
  13. • 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?
  14. 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
  15. 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
  16. 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
  17. 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:
  18. 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"
  19. 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
  20. 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