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?
● 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”?
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
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
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
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
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
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"
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
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
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 "" ()
● 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?
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
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
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
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"
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
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