44 | | Record field constraints `r { x :: t }` are syntactic sugar for typeclass constraints `Has r "x" t`, where |
45 | | |
46 | | {{{ |
47 | | class Has (r :: *) (x :: Symbol) (t :: *) where |
48 | | getFld :: r -> t |
49 | | }}} |
50 | | |
51 | | Recall that `Symbol` is the kind of type-level strings. The notation extends to conjunctions: `r {x :: tx, y :: ty}` means `(Has r "x" tx, Has r "y" ty)`. Note also that `r` and `t` might be arbitrary types, not just type variables or type constructors. For example, `T (Maybe v) { x :: [Maybe v] }` means `(Has (T (Maybe b)) "x" [Maybe v])`. |
52 | | |
53 | | Instances for the `Has` typeclass are automatically generated (for modules with `-XOverloadedRecordFields` enabled) using the record fields that are in scope. For example, the data type |
| 44 | Record field constraints `r { x :: t }` are syntactic sugar for typeclass constraints `Get r "x" t`, where |
| 45 | |
| 46 | {{{ |
| 47 | type family GetResult (r :: *) (f :: Symbol) :: * |
| 48 | |
| 49 | class t ~ GetResult r f => Get r (f :: Symbol) t where |
| 50 | getFld :: proxy f -> r -> t |
| 51 | }}} |
| 52 | |
| 53 | Recall that `Symbol` is the kind of type-level strings. The notation extends to conjunctions: `r {x :: tx, y :: ty}` means `(Get r "x" tx, Get r "y" ty)`. Note also that `r` and `t` might be arbitrary types, not just type variables or type constructors. For example, `T (Maybe v) { x :: [Maybe v] }` means `(Get (T (Maybe b)) "x" [Maybe v])`. |
| 54 | |
| 55 | Instances for the `Get` typeclass and `GetResult` type family are automatically generated (for modules with `-XOverloadedRecordFields` enabled) using the record fields that are in scope. For example, the data type |
66 | | The `(b ~ [a])` in the instance is important, so that we get an instance match from the first two fields only. For example, if the constraint `Has (T c) "x" d` is encountered during type inference, the instance will match and generate the constraints `(a ~ c, b ~ d, b ~ [a])`. |
| 69 | The `(b ~ [a])` in the instance is important, so that we get an instance match from the first two parameters only. For example, if the constraint `Get (T c) "x" d` is encountered during type inference, the instance will match and generate the constraints `(a ~ c, b ~ d, b ~ [a])`. Moreover, the `GetResult` type family ensures that the third parameter is functionally dependent on the first two, which is needed to avoid ambiguity errors when composing overloaded fields. |
| 70 | |
| 71 | The reason for using a three-parameter class, rather than just two parameters and a type family, is to support the syntactic sugar. With a two-parameter class we could easily end up inferring types like the following, and it would be hard to reapply the sugar: |
| 72 | |
| 73 | {{{ |
| 74 | f :: (Has r "x", Has r "y", GetResult r "x" ~ Int, GetResult r "y" ~ Int) => r -> Int |
| 75 | f r = x r + y r :: Int |
| 76 | }}} |