my blog lives here now
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

191 lines
5.3 KiB

6 years ago
  1. ---
  2. title: "A Quickie: Manipulating Records in Amulet"
  3. date: September 22, 2019
  4. maths: true
  5. ---
  6. Amulet, unlike some [other languages], has records figured out. Much like
  7. in ML (and PureScript), they are their own, first-class entities in the
  8. language as opposed to being syntax sugar for defining a product
  9. constructor and projection functions.
  10. ### Records are good
  11. Being entities in the language, it's logical to characterize them by
  12. their introduction and elimination judgements[^1].
  13. Records are introduced with record literals:
  14. $$
  15. \frac{
  16. \Gamma \vdash \overline{e \downarrow \tau}
  17. }{
  18. \Gamma \vdash \{ \overline{\mathtt{x} = e} \} \downarrow \{ \overline{\mathtt{x} : \tau} \}
  19. }
  20. $$
  21. And eliminated by projecting a single field:
  22. $$
  23. \frac{
  24. \Gamma \vdash r \downarrow \{ \alpha | \mathtt{x} : \tau \}
  25. }{
  26. \Gamma \vdash r.\mathtt{x} \uparrow \tau
  27. }
  28. $$
  29. Records also support monomorphic update:
  30. $$
  31. \frac{
  32. \Gamma \vdash r \downarrow \{ \alpha | \mathtt{x} : \tau \}
  33. \quad \Gamma \vdash e \downarrow \tau
  34. }{
  35. \Gamma \vdash \{ r\ \mathtt{with\ x} = e \} \downarrow \{ \alpha | \mathtt{x} : \tau \}
  36. }
  37. $$
  38. ### Records are.. kinda bad?
  39. Unfortunately, the rather minimalistic vocabulary for talking about
  40. records makes them slightly worthless. There's no way to extend a
  41. record, or to remove a key; Changing the type of a key is also
  42. forbidden, with the only workaround being enumerating all of the keys
  43. you _don't_ want to change.
  44. And, rather amusingly, given the trash-talking I pulled in the first
  45. paragraph, updating nested records is still a nightmare.
  46. ```amulet
  47. > let my_record = { x = 1, y = { z = 3 } }
  48. my_record : { x : int, y : { z : int } }
  49. > { my_record with y = { my_record.y with z = 4 } }
  50. _ = { x = 1, y = { z = 4 } }
  51. ```
  52. Yikes. Can we do better?
  53. ### An aside: Functional Dependencies
  54. Amulet recently learned how to cope with [functional dependencies].
  55. Functional dependencies extend multi-param type classes by allowing the
  56. programmer to restrict the relationships between parameters. To
  57. summarize it rather terribly:
  58. ```amulet
  59. (* an arbitrary relationship between types *)
  60. class r 'a 'b
  61. (* a function between types *)
  62. class f 'a 'b | 'a -> 'b
  63. (* a one-to-one mapping *)
  64. class o 'a 'b | 'a -> 'b, 'b -> 'a
  65. ```
  66. ### Never mind, records are good
  67. As of [today], Amulet knows the magic `row_cons` type class, inspired by
  68. [PureScript's class of the same name].
  69. ```amulet
  70. class
  71. row_cons 'record ('key : string) 'type 'new
  72. | 'record 'key 'type -> 'new (* 1 *)
  73. , 'new 'key -> 'record 'type (* 2 *)
  74. begin
  75. val extend_row : forall 'key -> 'type -> 'record -> 'new
  76. val restrict_row : forall 'key -> 'new -> 'type * 'record
  77. end
  78. ```
  79. This class has built-in solving rules corresponding to the two
  80. functional dependencies:
  81. 1. If the original `record`, the `key` to be inserted, and its
  82. `type` are all known, then the `new` record can be solved for;
  83. 2. If both the `key` that was inserted, and the `new` record, it is
  84. possible to solve for the old `record` and the `type` of the `key`.
  85. Note that rule 2 almost lets `row_cons` be solved for in reverse. Indeed, this is expressed by the type of `restrict_row`, which discovers both the `type` and the original `record`.
  86. Using the `row_cons` class and its magical methods...
  87. 1. Records can be extended:
  88. ```amulet
  89. > Amc.extend_row @"foo" true { x = 1 }
  90. _ : { foo : bool, x : int } =
  91. { foo = true, x = 1 }
  92. ```
  93. 2. Records can be restricted:
  94. ```amulet
  95. > Amc.restrict_row @"x" { x = 1 }
  96. _ : int * { } = (1, { x = 1 })
  97. ```
  98. And, given [a suitable framework of optics], records can be updated
  99. nicely:
  100. ```amulet
  101. > { x = { y = 2 } } |> (r @"x" <<< r @"y") ^~ succ
  102. _ : { x : { y : int } } =
  103. { x = { y = 3 } }
  104. ```
  105. ### God, those are some ugly types
  106. It's worth pointing out that making an optic that works for all fields,
  107. parametrised by a type-level string, is not easy or pretty, but it is
  108. work that only needs to be done once.
  109. ```ocaml
  110. type optic 'p 'a 's <- 'p 'a 'a -> 'p 's 's
  111. class
  112. Amc.row_cons 'r 'k 't 'n
  113. => has_lens 'r 'k 't 'n
  114. | 'k 'n -> 'r 't
  115. begin
  116. val rlens : strong 'p => proxy 'k -> optic 'p 't 'n
  117. end
  118. instance
  119. Amc.known_string 'key
  120. * Amc.row_cons 'record 'key 'type 'new
  121. => has_lens 'record 'key 'type 'new
  122. begin
  123. let rlens _ =
  124. let view r =
  125. let (x, _) = Amc.restrict_row @'key r
  126. x
  127. let set x r =
  128. let (_, r') = Amc.restrict_row @'key r
  129. Amc.extend_row @'key x r'
  130. lens view set
  131. end
  132. let r
  133. : forall 'key -> forall 'record 'type 'new 'p.
  134. Amc.known_string 'key
  135. * has_lens 'record 'key 'type 'new
  136. * strong 'p
  137. => optic 'p 'type 'new =
  138. fun x -> rlens @'record (Proxy : proxy 'key) x
  139. ```
  140. ---
  141. Sorry for the short post, but that's it for today.
  142. ---
  143. [^1]: Record fields $\mathtt{x}$ are typeset in monospaced font to make
  144. it apparent that they are unfortunately not first-class in the language,
  145. but rather part of the syntax. Since Amulet's type system is inherently
  146. bidirectional, the judgement $\Gamma \vdash e \uparrow \tau$ represents
  147. type inference while $\Gamma \vdash e \downarrow \tau$ stands for type
  148. checking.
  149. [functional dependencies]: https://web.cecs.pdx.edu/~mpj/pubs/fundeps.html
  150. [other languages]: https://haskell.org
  151. [today]: https://github.com/tmpim/amulet/pull/168
  152. [PureScript's class of the same name]: https://pursuit.purescript.org/builtins/docs/Prim.Row#t:Cons
  153. [a suitable framework of optics]: /static/profunctors.ml.html