|
| 1 | +# Labeled Tuple Projections |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +This RFC proposes a quality-of-life improvement to OCaml's tuples, adding |
| 6 | +support for labeled tuple projections. |
| 7 | + |
| 8 | +## Proposed change |
| 9 | + |
| 10 | +The idea is to allow users to directly project elements from a labeled tuple |
| 11 | +using labels, as opposed to patterns: |
| 12 | + |
| 13 | +```ocaml |
| 14 | +# let x = (~tuple:42, ~proj:1337, "is", 'c', 00, 1);; |
| 15 | +val x : (tuple: int * proj:int * string * char * int * int) = |
| 16 | + (~tuple:42, ~proj:1337, "is", 'c', 0, 1) |
| 17 | +# x.tuple;; |
| 18 | +- : int = 42 |
| 19 | +``` |
| 20 | + |
| 21 | +Here, we're able to project out of a 6-tuple (containing both labeled and |
| 22 | +unlabeled components) simply by writing `x.l` (for a label `l`). |
| 23 | + |
| 24 | +This is useful for a couple reasons: |
| 25 | +- Clarity: `x.label` is more readable than `let (~label, ..) = x in ...` |
| 26 | +- Parity with records: complements record field projection |
| 27 | + |
| 28 | +**A historical note**: an earlier proposal also explored projections |
| 29 | +for *unlabeled* tuples, along with an empirical analysis of projection |
| 30 | +patterns within the ecosystem. Since labeled tuples are a recent addition, |
| 31 | +a similar analysis is not yet useful for motivating this feature. |
| 32 | + |
| 33 | +## Previous work |
| 34 | + |
| 35 | +Many other strongly-typed languages support built-in tuple projections. |
| 36 | + |
| 37 | +### SML |
| 38 | + |
| 39 | +SML models tuples as records with integer field names (1-indexed), so projection uses |
| 40 | +record selection syntax: |
| 41 | +```sml |
| 42 | +> val x = (1, "hi", 42);; |
| 43 | +val x = (1, "hi", 42): int * string * int |
| 44 | +> val y = #1 x;; |
| 45 | +val y = 1: int |
| 46 | +> val z = #3 x |
| 47 | +val z = 42: int |
| 48 | +``` |
| 49 | + |
| 50 | +### Rust |
| 51 | + |
| 52 | +Rust supports tuple projections (0-indexed) for ordinary tuples (and tuple structs): |
| 53 | +```rust |
| 54 | +let x = (42, "is", 'c'); |
| 55 | +let y = x.0; // 42 |
| 56 | +let z = x.1; // "is" |
| 57 | + |
| 58 | +struct Point(i32, i32); |
| 59 | +let p = Point(3, 4); |
| 60 | +let x_coord = p.0; // 3 |
| 61 | +``` |
| 62 | + |
| 63 | +Record structs also use the same syntax for projection: |
| 64 | +```rust |
| 65 | +struct Point { x : i32, y : i32 }; |
| 66 | +let p = Point { x = 3, y = 4 }; |
| 67 | +let x_coord = p.x; // 3 |
| 68 | +``` |
| 69 | + |
| 70 | + |
| 71 | +### Swift |
| 72 | + |
| 73 | +Swift supports tuple projections via both positional indicies and labels (as in this proposal): |
| 74 | +```swift |
| 75 | +import Foundation |
| 76 | + |
| 77 | +let x = (tuple: 42, proj: 1337, "is", 'c', 00, 1); |
| 78 | + |
| 79 | +print(x.tuple) // 42 |
| 80 | +print(x.5) // 1 |
| 81 | +``` |
| 82 | + |
| 83 | +## Implementation |
| 84 | + |
| 85 | +An experimental implementation is available at [PR 14257](https://github.com/ocaml/ocaml/pull/14257). |
| 86 | + |
| 87 | +### Parsetree changes |
| 88 | + |
| 89 | +Given the syntax for labeled tuple projection is overloaded with record field |
| 90 | +projection, i.e. there is no syntactic distinction between the projections in: |
| 91 | +```ocaml |
| 92 | +let x = { foo = 1; bar = 2 } in x.foo;; |
| 93 | +``` |
| 94 | +and |
| 95 | +```ocaml |
| 96 | +let x = ~foo:1, ~bar:2 in x.foo;; |
| 97 | +``` |
| 98 | + |
| 99 | +### Typechecking |
| 100 | + |
| 101 | +While typechecking, when encountering a field projection in expressions, |
| 102 | + |
| 103 | +- If the field is a record or tuple label `l`. |
| 104 | + |
| 105 | + Check to see whether the expected type is known: |
| 106 | + - If the type is not known: typecheck the projection `e.l` as a record projection |
| 107 | + - If the type is known to be `(ty0, ..., tyn) t`: ditto |
| 108 | + - If the type is known to be `(?l0:ty0 * ... * l:tyl * ... * ?ln:tyn)`: type the projection |
| 109 | + as `tyl`. |
| 110 | + |
| 111 | +## Considerations |
| 112 | + |
| 113 | +### Limitations of type-based disambiguation |
| 114 | + |
| 115 | +OCaml's current type-based disambiguation mechanism is relatively weak. As a result, |
| 116 | +many of the patterns that tuple projections are intended to replace would be ill-typed under |
| 117 | +today's implementation. For instance: |
| 118 | +```ocaml |
| 119 | +# List.map (fun x -> x.num) [(~num:42, "Hello"); (~num:1337, "World")];; |
| 120 | +Error: The type of the tuple expression is ambiguous. |
| 121 | + Could not determine the type of the tuple projection. |
| 122 | +``` |
| 123 | + |
| 124 | +That said, this limitation does not arise from the feature itself, but from the |
| 125 | +weaknesses in OCaml's type propagation. Improving type propagation (separately) |
| 126 | +would benefit not only tuple projections, but other features that rely on |
| 127 | +type-based disambiguation (e.g. constructors and record fields). As such, we |
| 128 | +argue that tuple projections should not be rejected on this point alone, and |
| 129 | +that the broader issues of type propagation and disambiguation be addressed |
| 130 | +separately. |
| 131 | + |
| 132 | +### Syntactic overloading |
| 133 | + |
| 134 | +This proposal reuses the existing projection syntax `e.l` for both record |
| 135 | +fields and labeled tuples. The primary motivator behind this is to avoid |
| 136 | +introducing new operators and keeps projection syntax uniform. |
| 137 | + |
| 138 | +The downside is that it increases reliance on type-based disambiguation. |
| 139 | + |
| 140 | +### Diagnostic quality of error messages |
| 141 | + |
| 142 | +Type errors surrounding unknown fields will need to be refined. |
| 143 | +In particular, when the compiler defaults a labeled projection to a record |
| 144 | +field (even though it might also have been a labeled tuple projection), |
| 145 | +the diagnostic report ought to make this clear. |
| 146 | + |
| 147 | +Otherwise, programs like the following may yield cryptic messages: |
| 148 | +```ocaml |
| 149 | +# let is_ill_typed_due_to_defaults x = |
| 150 | + let y = x.tuple_label_a in |
| 151 | + ignore (x : (tuple_label_a:int * string * bool)); |
| 152 | + y |
| 153 | +Error: Unbound record field `tuple_label_a` |
| 154 | +``` |
| 155 | + |
| 156 | +A clearer diagnostic could be: |
| 157 | +``` |
| 158 | +Error: The field `tuple_label_a` is unknown. |
| 159 | + The projection `x.tuple_label_a` was interpreted as a record field, |
| 160 | + but no such record field exists. |
| 161 | +
|
| 162 | +Hint: Did you mean to project from a labeled tuple instead? |
| 163 | + If so, add an annotation to disambiguate the projection. |
| 164 | +``` |
| 165 | + |
| 166 | +Other problematic examples include conflicts with existing records: |
| 167 | +```ocaml |
| 168 | +# type discombobulating_record = { tuple_label_a : int };; |
| 169 | +type discombobulating_record = { tuple_label_a : int } |
| 170 | +# let is_ill_typed_due_to_defaults x = |
| 171 | + let y = x.tuple_label_a in |
| 172 | + ignore (x : (tuple_label_a:int * string * bool)); |
| 173 | + y |
| 174 | +Error: The value `x` has type `discombobulating_record` but an expression was |
| 175 | + expected of type `tuple_label_a:int * string * bool` |
| 176 | +``` |
| 177 | +Here the error conflates record and tuple typing, which is misleading. |
| 178 | +A more informative report could combine a warning with the final error: |
| 179 | +```ocaml |
| 180 | +Warning: The projection `x.tuple_label_a` could refer either to a record field |
| 181 | + or a labeled tuple component. It was resolved as a record field of |
| 182 | + `discombobulating_record`. |
| 183 | + Please disambiguate if this is wrong. |
| 184 | +Error: The value `x` has type `discombobulating_record` but an expression |
| 185 | + was expected of type |
| 186 | + `tuple_label_a:int * string * bool` |
| 187 | +``` |
| 188 | + |
0 commit comments