Slide 1

Slide 1 text

Type-safe, Type-level, Type-driven solutions with PureScript Justin Woo (우/禹) 14 Jul 18 Taipei @ FEDC

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

This is not a normal introduction

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

What this talk is ● Talk generally about what PureScript is ● Talk about interesting things from PureScript missing from most languages ○ Some of these ideas overlap with Haskell ○ Not very much with others, unfortunately ● Show you how the computer can do repetitive and error-prone things for you with just type definitions ○ Without a bunch of code generation (or glorified lookalikes) ● Hopefully get you excited to learn more about PureScript

Slide 7

Slide 7 text

What is PureScript? ● A language similar to Haskell that compiles to JS ○ But with many different details, some due to later implementation ○ Implements some ideas newer than when Haskell’s core work was done ● Unlike annotation-based languages, Types are first class ○ Match and use actual constructors, not only describe some existing untyped code ○ Doesn’t work by trying to type guard with limited information -- fundamental information builds up ● Gives you tools to encode more information in your codebase ○ “Make invalid states impossible”: you can make this a reality ● A culture of “sky’s the limit” ○ No thought-leaders to tell you you’re wrong ○ You can always ask about what you should try doing ○ Everyone wants you to learn more

Slide 8

Slide 8 text

“Make invalid states impossible” ● This isn’t a binary property, and some languages don’t give you enough power ● In reality, a spectrum of how much you choose to encode at this time ● Type alias: least safety, greatest convenience type URL1 = String f1 :: URL1 -> String wrong1 = f1 "sadf"

Slide 9

Slide 9 text

● Newtype: where constructors are exported or not, for explicit identification newtype URL2 = URL2 String f2 :: URL2 -> String -- only as good as mkURL2 :: String -> Either Error URL2 ● Refinements: where constructor should not be exported, where structural subtypes of validations can be required data HasAtSign data IsGmail newtype URL3 (validations :: # Type) = URL3 String f3 :: forall r. URL3 (hasAtSign :: HasAtSign, isGmail :: IsGmail | r) -> String ● What is “# Type”?

Slide 10

Slide 10 text

What is a “# Type” aka. row type of Type? ● Think about records: ○ Statically defined fields with static label names ○ Heterogeneous, where each field can have different type ○ But each field will always be the correct Type type MyRecord = { apple :: Int -- always defined Int e.g. 1,2,3 , banana :: Boolean -- always defined Boolean, true/false , cherry :: Maybe String -- maybe defined String, Nothing/Just "asdf" }

Slide 11

Slide 11 text

● But { fields… } syntax is just sugar for records which are parameterized by this row type data Record :: # Type -> Type type MyRealRecord = Record ( apple :: Int , banana :: Boolean , cherry :: Maybe String )

Slide 12

Slide 12 text

Making invalid states impossible, part 2 ● Think cardinality: is it valid to have independent fields, or is it a sum type? data IntOrBoolean = IsInt Int | IsBoolean Boolean -- N + 2 states type MyData1 = { isSubmitted :: IntOrBoolean -- N + 2 , date :: JSDate -- M } -- (N + 2) * M data MyData2 = Submitted JSDate | NotSubmitted -- M + 1

Slide 13

Slide 13 text

● Is it actually a homogeneous Map or is it actually a Record? -- for given key, undefined or int or boolean? type MyThings1 = Map String IntOrBoolean lookupMT1 :: String -> Map String IntOrBoolean -> Maybe IntOrBoolean lookupMT1 = lookup type MyThings2 = { apples :: Int , isChairInRoom :: Boolean } -- apples, isChairInRoom are always defined and are int, boolean ● Quick check: if you ever want Map of a sum type key or value but you already know what keys you have, it's almost always a record

Slide 14

Slide 14 text

Types aren’t just for checking your work

Slide 15

Slide 15 text

Decoding JSON should be free ● If you have static information available about the type you want, you should be able to derive operations for free ○ And it should be done in user libraries, not hardcoded in the compiler type MyJSON = { apple :: String , banana :: Int , cherry :: Maybe Boolean } decodeToMyJSON :: String -> Either (NonEmptyList ForeignError) MyJSON decodeToMyJSON = SimpleJSON.readJSON

Slide 16

Slide 16 text

main = do logShow $ decodeToMyJSON """{ "apple": "red", "banana": 2 } """ -- (Right { apple: "red", banana: 2, cherry: (Just true) }) logShow $ decodeToMyJSON """{ "apple": "red", "banana": "wrong" } """ -- (Left (NonEmptyList(NonEmpty -- (ErrorAtProperty "banana" (TypeMismatch "Int" "String")) Nil))) ● How does this work?

Slide 17

Slide 17 text

Type-driven programs ● Simple-JSON and many other libraries work by using type classes, which resolve based on concrete contexts class ReadForeign a where readImpl :: Foreign -> F a instance readString :: ReadForeign String where instance readArray :: ReadForeign a => ReadForeign (Array a) where instance readMaybe :: ReadForeign a => ReadForeign (Maybe a) where instance readRecord :: ( RowToList fields fieldList , ReadForeignFields fieldList () fields ) => ReadForeign (Record fields) ● What is RowToList?

Slide 18

Slide 18 text

What is RowToList? ● We can work with the static row type information, make it an iterable list at the type level, and match that to instances toRLProxy :: forall row rl. RowToList row rl => Proxy { | row } -> RLProxy rl toRLProxy _ = RLProxy rlMyJSON :: RLProxy (Cons "apple" String (Cons "banana" Int (Cons "cherry" (Maybe Boolean) Nil))) rlMyJSON = toRLProxy (Proxy :: Proxy MyJSON) instance [...] ReadForeignFields (Cons name ty tail) instance [...] ReadForeignFields (Nil)

Slide 19

Slide 19 text

Other examples of type-driven programs ● We can do a lot of things with this: ○ Type-level routing between server endpoints and frontend requests ○ Implement a type-safe Cycle.js (drivers -> sources, sinks -> drivers) ○ Refinement types (validation in row types, run validation per field) ○ Decode INI files ○ Diff two differently typed records ○ Exhaustively match polymorphic variant members ○ And more! ● More and more exciting things are possible with the combination of static type information and “type-level programming”

Slide 20

Slide 20 text

Type synthesis with other type information ● This type signature is inferred: 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 " ● speakerdeck.com/justinwoo/superior-string-spaghetti-with-purescript-jajanmen

Slide 21

Slide 21 text

This is just the tip of the iceberg

Slide 22

Slide 22 text

Learn PureScript! ● Various ways to use PureScript ○ Small modules that you import in to existing projects (100% interop) ○ Hoisting of PureScript in JS or JS in PureScript ○ Small applications with existing libraries ● Requires some upfront investment ○ Reading, doing some practice problems, looking at core libraries like PureScript-Effect ○ Learning about FFI, i.e. foreign import name :: _ <> exports.name = … ■ This has immediate payoff: you can write anything for browser/Node after learning this ● Requires some community engagement ○ Local usergroups: facebook.com/groups/PureScript.tw ○ Discourse: purescript-users.ml ○ #purescript, #purescript-beginners on FP Chat:fpchat-invite.herokuapp.com ○ Ask me anything: twitter.com/jusrin00

Slide 23

Slide 23 text

Thanks!