Type-safe, Type-level, Type-driven solutions with PureScript

Type-safe, Type-level, Type-driven solutions with PureScript

Talk I gave at FEDC (Front End Design Conf) 2018 in Taiwan

Description from FEDC website:

In this talk, Justin will talk about PureScript and how many problems we solve can be generalized and solved safely using types -- not only to check individual implementations, but to derive implementations automatically using type-level information. Justin will show some examples of how problems we solve with error-prone manual approaches and buggy codegen can instead be solved first-class when using an advanced language like PureScript.


Justin Woo

July 14, 2018


  1. Type-safe, Type-level, Type-driven solutions with PureScript Justin Woo (우/禹) 14

    Jul 18 Taipei @ FEDC
  2. None
  3. None
  4. This is not a normal introduction

  5. None
  6. 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
  7. 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
  8. “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"
  9. • 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”?
  10. 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" }
  11. • 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 )
  12. 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
  13. • 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
  14. Types aren’t just for checking your work

  15. 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
  16. 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?
  17. 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?
  18. 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)
  19. 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”
  20. 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
  21. This is just the tip of the iceberg

  22. 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
  23. Thanks!