Skip to content

Commit 22b0f11

Browse files
committed
Labeled tuple projection draft
1 parent 308ebe9 commit 22b0f11

File tree

1 file changed

+188
-0
lines changed

1 file changed

+188
-0
lines changed

rfcs/labeled_tuple_projections.md

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
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

Comments
 (0)