$30 off During Our Annual Pro Sale. View Details »

RowList Fun With PureScript 2nd Edition

Justin Woo
November 24, 2017

RowList Fun With PureScript 2nd Edition

Modified version of the RowList talk I gave at Zendesk

More information about RowList and other row type operations in PureScript here: https://github.com/justinwoo/awesome-rowlist

Justin Woo

November 24, 2017
Tweet

More Decks by Justin Woo

Other Decks in Programming

Transcript

  1. RowList Fun with
    PureScript
    Justin Woo
    Orig: Small FP Conf 2017

    View Slide

  2. View Slide

  3. View Slide

  4. View Slide

  5. Øredev 2017
    Kris Jenkins
    PureScript: Tomorrow’s JavaScript Today
    https://vimeo.com/243148125

    View Slide

  6. What is PureScript?
    ● Pure, Strict, Functional
    ● Higher Kinded Types
    ● Type classes with functional dependencies
    ● Tooling with purescript-ide server + client
    ● Docs for packages on Pursuit
    ● Easy-to-use build tool in Pulp
    ● Typed holes with type-directed search
    ● No runtime -- compiles to real JS
    ● Row types
    ● ...and more!

    View Slide

  7. What are Row types?
    ● An unordered collection of fields
    ○ Key is Symbol (a type-level string kind)
    ○ Value is of the kind provided to the row
    data Record :: # Type -> Type
    -- takes row of Type to define a Type
    type Person =
    { name :: String, age :: Number }
    type Person =
    Record (name :: String, age :: Number)
    ● Eff works this way!
    kind Effect
    data HTTP :: Effect
    data Eff :: # Effect -> Type -> Type
    main :: Eff ( http :: HTTP ) Unit

    View Slide

  8. Collection? How about as a List?
    ● What if we could use these rows as lists of fields? Wouldn’t that be fun?
    kind RowList
    data Nil :: RowList
    data Cons :: Symbol -> Type -> RowList -> RowList
    type PersonRow = (name :: String, age :: Number)
    type PersonRowList = (Cons "name" String (Cons "age" Number Nil))

    View Slide

  9. Type classes: “type-level case statements”
    ● How do you match and work with things on the type level? Type classes!
    ○ Using instances for each branch
    ● Simple example: getting keys from a record
    class Keys (xs :: RowList) where
    keysImpl :: RLProxy xs -> List String
    -- forall g. g xs -> List String
    instance nilKeys :: Keys Nil where
    keysImpl _ = mempty
    -- Phantom types reminder
    data Maybe a = Nothing | Just a
    data Proxy a = Proxy
    data RLProxy (xs :: RowList) = RLProxy

    View Slide

  10. Cons instance of class Keys
    1. Pattern match on the RowList
    2. Reflect the Symbol to a string value
    3. Get the rest by calling keysImpl on the tail
    4. Cons this string onto the rest of the keys
    That’s it!
    instance consKeys ::
    ( IsSymbol name
    , Keys tail
    ) => Keys (Cons name ty tail) where
    keysImpl _ = first : rest
    where
    first = reflectSymbol (SProxy :: SProxy name)
    rest = keysImpl (RLProxy :: RLProxy tail)

    View Slide

  11. Exposing our method in a reasonable way
    ● We wrote a class for RowList, but this needs to work for row types from Records
    ● RowToList allows us to convert row to RowList for exactly this!
    keys :: forall row rl. RowToList row rl => Keys rl => Record row -> List String
    keys _ = keysImpl (RLProxy :: RLProxy rl)
    ● Since we have everything from the type level, the record ends up being a glorified Proxy

    View Slide

  12. Keys in action
    Usage:
    main =
    traverse_ print $ keys { a: 1, b: 2 }
    Output:
    "a"
    "b"

    View Slide

  13. Simple-JSON:
    Get JSON de/serialization for free for record type
    aliases and more!
    Sample Applications of RowLists (1 of n)
    class ReadForeignFields (xs :: RowList) (from :: # Type)
    (to :: # Type)
    | xs -> from to where
    getFields :: RLProxy xs
    -> Foreign
    -> F (Builder (Record from) (Record to))

    View Slide

  14. OhYes:
    Constrain PureScript types to be directly
    interoperable with TypeScript, with support for
    union types with a “type” string literal field using
    Variant
    (Same with Kancho, for Elm ports)
    Sample Applications of RowLists (2 of n)
    type VariantTest = Variant
    ( a :: String
    , b :: Number
    , c :: Boolean
    )
    export type VariantTest =
    | { type: "a", value: string }
    | { type: "b", value: number }
    | { type: "c", value: boolean };

    View Slide

  15. ChocoPie/Cycle-Run:
    Libraries for creating a cycle of Events, such that
    sources -> sinks, sink -> IO source for each pair
    Sample Applications of RowLists (3 of n)
    instance cycleRunRowListCons ::
    ( CycleRunRowList sourceTail sinkTail driverTail
    ) => CycleRunRowList
    (Cons k b sourceTail)
    (Cons k (Stream a) sinkTail)
    (Cons k ((Stream a) -> Eff e b) driverTail)

    View Slide

  16. HomeRunBall:
    Define validations to be performed on a value using
    row types and have them run. Then use the phantom
    types to constrain code.
    Sample Applications of RowLists (4 of n)
    onlyOnApples ::
    ValidatedValue
    (beginsApple :: BeginsWith "Apple")
    String
    -> String
    onlyOnApples _ = "U R COOL"

    View Slide

  17. ...and so much more!

    View Slide

  18. “This seems like a lot of work for
    something I do with objects/hashes/etc”
    Records are not hashes/objects and vice versa
    ● Records are mostly heterogenous, objects/hashes are homogenous
    ○ “I handle multiple types” -- three options there
    ■ Dynamic/Foreign/Object -- a.k.a. Implicit coercion hell
    ■ Polymorphic variants -- I use this sometimes too, but… why?
    ■ Sum types -- Pretty useful, but still not ideal
    ○ If you use any of the above, you now have N * M possible values instead of N (N fields, each field can be one of
    M)
    ○ Common mistake: mixing up multiple values of a type with multiple types of a kind
    ■ Can’t naively coerce most records into string maps, though that can be very useful!
    ■ But you will never convert a string map to a record without parsing or unsafe coercion

    View Slide

  19. ● Static keys -- there are explicit guarantees of keys existing in a record
    ○ Lookups go to Maybe/Option/Nullable
    ○ “I know it exists” -- three options here
    ■ Unsafe coercion from Maybe a to a
    ● Don’t do this because you will just crash prod forever
    ■ Coercion from phantom type parameter evidence
    ● newtype MyMap (alwaysDefined :: # Cond) = MyMap (StrMap a)
    getApple :: forall a cond trash alwaysDefined
    . RowCons “apple” cond trash alwaysDefined
    => MyMap alwaysDefined a -> a
    ■ Refinement types
    ● Hope you like Liquid Haskell
    Cont.

    View Slide

  20. “Will I need to do this if I [use / start using]
    Purescript?”
    ● Writing type-level code using RowList?
    ○ Not really, but sometimes
    ○ And you will learn these from usage
    ● Defining your own type classes and working with them?
    ○ Not necessarily, but sometimes
    ○ And you will learn these from usage
    ● Understanding how to use constraints and throw them around?
    ○ Yes and no
    ○ You will learn these from usage
    ○ You can always start with the most concrete types and use more general ones over time
    ■ e.g. Monoid mempty vs []
    ● Even if you don’t use these directly, libraries will be able to provide you a whole lot by having these
    available

    View Slide

  21. FAQ-ish
    ● “Surely, we already had this with row polymorphism in other languages?”
    ○ Sadly, no, seems not.
    ○ What other language has the combination of
    ■ Generic row polymorphism i.e. not only Record (row :: # Type)
    ■ Higher kinded types
    ■ Ad-hoc polymorphism with extra features e.g. MultiParam Type Classes with Functional Dependencies
    ○ Nothing seems to come close, other than some Haskell libraries
    ■ Unless you want to get into codegen territory, which already throws away composability
    ● “So what if I don’t have this, what can I do?”
    ○ Haskellers might very well choose to implement a subset of features available through two methods
    ■ Heterogenous type-level lists with a linear search constraint (via type families or whatever)
    ● Could be combined with GHC Generics/Generics-Rep for effect?
    ■ Generics-Sums of Products + Records-SOP packages
    ○ Codegen like hell

    View Slide

  22. But PureScript is here today, so
    you don’t have to wait for tool X
    to develop features for working
    with row types!

    View Slide

  23. Thanks!
    Ask me Purescript questions on Twitter: @jusrin00
    IRC: #purescript on freenode
    Slack: #purescript on Functional Programming Slack
    Reddit: /r/purescript

    View Slide