RowList Fun With PureScript 2nd Edition

3b48c91bf6b6f0bfd0fda50625598656?s=47 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:


Justin Woo

November 24, 2017


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

  2. None
  3. None
  4. None
  5. Øredev 2017 Kris Jenkins PureScript: Tomorrow’s JavaScript Today

  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!
  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
  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))
  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
  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)
  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
  12. Keys in action Usage: main = traverse_ print $ keys

    { a: 1, b: 2 } Output: "a" "b"
  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))
  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 };
  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)
  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"
  17. ...and so much more!

  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
  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.
  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
  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
  22. But PureScript is here today, so you don’t have to

    wait for tool X to develop features for working with row types!
  23. Thanks! Ask me Purescript questions on Twitter: @jusrin00 IRC: #purescript

    on freenode Slack: #purescript on Functional Programming Slack Reddit: /r/purescript