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.

3b48c91bf6b6f0bfd0fda50625598656?s=128

Justin Woo

July 01, 2018
Tweet

Transcript

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

    ITA Milan
  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?
  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”?
  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
  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
  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
  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
  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
  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"
  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
  11. “But we could use QuasiQuotes” Mà, ho portato dei Tipi

    per gli spaghetti
  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
  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
  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 "" ()
  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?
  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
  17. RowCons in a nutshell

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