Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Type-safe, Type-level, Type-driven solutions wi...

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.

Avatar for Justin Woo

Justin Woo

July 14, 2018
Tweet

More Decks by Justin Woo

Other Decks in Programming

Transcript

  1. 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
  2. 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
  3. “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"
  4. • 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”?
  5. 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" }
  6. • 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 )
  7. 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
  8. • 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
  9. 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
  10. 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?
  11. 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?
  12. 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)
  13. 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”
  14. 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
  15. 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