Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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?

Slide 3

Slide 3 text

● 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”?

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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"

Slide 10

Slide 10 text

Why Jajanmen? Spaghetti Plain, Easily ruined by Americans Stringly typed spaghetti code Jajanmen Delicious, not yet ruined by Americans Strongly typed inferred record types

Slide 11

Slide 11 text

“But we could use QuasiQuotes” Mà, ho portato dei Tipi per gli spaghetti

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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 "" ()

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

RowCons in a nutshell

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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:

Slide 21

Slide 21 text

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"

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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