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

Superior string spaghetti with PureScript

Justin Woo
October 12, 2018

Superior string spaghetti with PureScript

Lightning talk I gave at HaskellX 2018

Justin Woo

October 12, 2018
Tweet

More Decks by Justin Woo

Other Decks in Programming

Transcript

  1. Superior string spaghetti with PureScript Justin Woo October 12 2018

    Justin Woo Superior string spaghetti with PureScript October 12 2018 1 / 17
  2. Problem: Untyped Parameterized SQL Queries We want to work with

    parameterized SQL queries, but they’re always untyped thing = 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? Justin Woo Superior string spaghetti with PureScript October 12 2018 2 / 17
  3. What if you could Cons Symbol like Lists? In PureScript

    0.12, we can! “ABC” -> “A” “BC” class Cons (head :: Symbol) (tail :: Symbol) (symbol :: Symbol | head tail -> symbol, symbol -> head tail Now we can parse Symbols! class ParseParamName (x :: Symbol) (xs :: Symbol) (acc :: Symbol) (out :: Symbol) | x xs -> acc out Justin Woo Superior string spaghetti with PureScript October 12 2018 3 / 17
  4. The problem with normal instance 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 Justin Woo Superior string spaghetti with PureScript October 12 2018 4 / 17
  5. Instance chains at work In PureScript 0.12, we have instance

    chains (groups) First come, first served, with regular fundep-based instance matching (not constraints/guards) instance endRParseParamName :: ( Symbol.Append acc x out ) => ParseParamName x "" acc out -- note the else here: else instance spaceParseParamName :: ParseParamName " " xs out out Instance chain (groups) can only be implemented within a module, which is fine Justin Woo Superior string spaghetti with PureScript October 12 2018 5 / 17
  6. How do these solve our problem? Now we can write

    a parser at the type level, and we can synthesize row types as usual: getEm :: forall a b . AllowedParamType a => AllowedParamType b => DBConnection -> { "$name" :: a , "$count" :: b } -> Aff Foreign getEm db = J.queryDB db $ SProxy :: SProxy """ select name, count from mytable where name = $name and count = $count """ Justin Woo Superior string spaghetti with PureScript October 12 2018 6 / 17
  7. Top level function queryDB :: forall query params . IsSymbol

    query => ExtractParams query params => SQLite3.DBConnection -> SProxy query -> { | params } -> Aff Foreign class ExtractParams (query :: Symbol) (params :: # Type) | query -> params instance extractParams :: ( Symbol.Cons x xs query , ExtractParamsParse x xs params ) => ExtractParams query params Justin Woo Superior string spaghetti with PureScript October 12 2018 7 / 17
  8. Parsing and Extracting Params class ExtractParamsParse (x :: Symbol) --

    current character (xs :: Symbol) -- tail (params :: # Type) -- row type of parsed parameters | x xs -> params -- fundeps, params are determined -- base case, no more to extract at end of string: instance endExtractParamsParse :: ExtractParamsParse x "" () Justin Woo Superior string spaghetti with PureScript October 12 2018 8 / 17
  9. 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 Otherwise, continue else instance nExtractParams :: ( Symbol.Cons y ys xs , ExtractParamsParse y ys row ) => ExtractParamsParse x xs row Justin Woo Superior string spaghetti with PureScript October 12 2018 9 / 17
  10. Row.Cons? Remember that record types are parameterized by row type

    in PureScript 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 Justin Woo Superior string spaghetti with PureScript October 12 2018 10 / 17
  11. 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 Justin Woo Superior string spaghetti with PureScript October 12 2018 12 / 17
  12. getEm :: forall a b . AllowedParamType a => AllowedParamType

    b => DBConnection -> { "$name" :: a, "$count" :: b } -> Aff Foreign getEm db = J.queryDB db $ SProxy :: SProxy """ select name, count from mytable where name = $name and count = $count """ Justin Woo Superior string spaghetti with PureScript October 12 2018 13 / 17
  13. 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 Justin Woo Superior string spaghetti with PureScript October 12 2018 15 / 17
  14. Thanks More detailed post here https://github.com/justinwoo/my-blog-posts# well-typed-parameterized-sqlite-parameters-with-purescript Csongor Kiss’s post

    on Symbol.Cons and his printf library: http://kcsongor.github.io/purescript-safe-printf/ Twitter: @jusrin00 Justin Woo Superior string spaghetti with PureScript October 12 2018 16 / 17
  15. Figure 3: Your face on PureScript Justin Woo Superior string

    spaghetti with PureScript October 12 2018 17 / 17