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