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

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
Tweet

More Decks by Justin Woo

Other Decks in Programming

Transcript

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

    View full-size slide

  2. This is not a
    normal
    introduction

    View full-size slide

  3. 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

    View full-size slide

  4. 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

    View full-size slide

  5. “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"

    View full-size slide

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

    View full-size slide

  7. 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"
    }

    View full-size slide

  8. ● 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
    )

    View full-size slide

  9. 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

    View full-size slide

  10. ● 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

    View full-size slide

  11. Types aren’t just
    for checking your
    work

    View full-size slide

  12. 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

    View full-size slide

  13. 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?

    View full-size slide

  14. 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?

    View full-size slide

  15. 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)

    View full-size slide

  16. 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”

    View full-size slide

  17. 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

    View full-size slide

  18. This is just the tip
    of the iceberg

    View full-size slide

  19. 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

    View full-size slide