1 | Tuples
|
---|
2 | ======
|
---|
3 |
|
---|
4 | Tuples were introduced by Dave Till in K-W C, added to CFA by Rodolfo Esteves,
|
---|
5 | and extended by Robert Schluntz. This proposal discusses updates for CFA tuples
|
---|
6 | to address problems that have appeared in their usage over the past 6 years.
|
---|
7 | The proposal attempts to address problems by adding new functionality, updating
|
---|
8 | existing features, and removing some problematic ones.
|
---|
9 |
|
---|
10 | The core change is breaking the current restructurable tuples into unstructured
|
---|
11 | and structured tuples. Unstructured tuples handle most of the existing uses,
|
---|
12 | with structured tuples filling in a few missing use cases.
|
---|
13 |
|
---|
14 | Current State of Tuples
|
---|
15 | -----------------------
|
---|
16 | An overview of the current tuples design is the starting place for the proposal.
|
---|
17 |
|
---|
18 | ### Tuple Syntax
|
---|
19 |
|
---|
20 | Currently, tuples have three main components: tuple types, tuple
|
---|
21 | expressions/values (constructing tuples), and tuple index expressions
|
---|
22 | (deconstructing tuples).
|
---|
23 |
|
---|
24 | Current syntax for tuple types.
|
---|
25 |
|
---|
26 | - Nullary: [void] or []
|
---|
27 | - Unary: [TYPE]
|
---|
28 | - Nary: [TYPE, TYPE, ...]
|
---|
29 |
|
---|
30 | Tuple types can appear in a function return and parameter declaration, or a
|
---|
31 | tuple variable declaration. Note, the Nullary tuple is only meaningful in the
|
---|
32 | return context,
|
---|
33 |
|
---|
34 | void f(...); // equivalent
|
---|
35 | [void] f(...);
|
---|
36 | [] f(...);
|
---|
37 |
|
---|
38 | as C does not support a void declaration.
|
---|
39 |
|
---|
40 | int f( void x ); // disallowed
|
---|
41 | int f( [void] x );
|
---|
42 | int f( [] x );
|
---|
43 |
|
---|
44 | Current syntax for tuple expressions:
|
---|
45 |
|
---|
46 | - Nullary: (Same as `void`, use return without an expression.)
|
---|
47 |
|
---|
48 | [] f( ) { return; }
|
---|
49 | [] f( ) { return [] ; }
|
---|
50 |
|
---|
51 | - Unary:
|
---|
52 |
|
---|
53 | [int] f( ) { return 3; }
|
---|
54 | [int] f( ) { return [3]; }
|
---|
55 |
|
---|
56 | - Nary: [EXPR, EXPR]
|
---|
57 |
|
---|
58 | [int,int] f( ) { return [3,4]; }
|
---|
59 |
|
---|
60 | Currently, there is a parsing problem for nullary and unary tuple expression,
|
---|
61 | which is being looked at. Hence, only these two forms work.
|
---|
62 |
|
---|
63 | [] f( ) { return; } // nullary
|
---|
64 | [int] f( ) { return 3; } // unary
|
---|
65 |
|
---|
66 | Current syntax for tuple indexing is an integer constant, where its value
|
---|
67 | selects a tuple member, e.g.:
|
---|
68 |
|
---|
69 | [char, int] tup;
|
---|
70 | tup = ['a', 0];
|
---|
71 | char ch = t.0; // select first tuple member
|
---|
72 | int value = t.1; // select second tuple member
|
---|
73 |
|
---|
74 | ### Mass and Multi-Assignment
|
---|
75 |
|
---|
76 | Two special forms of assignment can be used to set values in tuples: mass and
|
---|
77 | multi. Mass assignment assigns every element in the destination tuple to a
|
---|
78 | single source value.
|
---|
79 |
|
---|
80 | [int, long, float] dst;
|
---|
81 | int src = 4
|
---|
82 | dst = src; // => dst.0 = src; dst.1 = src; dst.2 = src
|
---|
83 |
|
---|
84 | Multi-assignment assigns every element in the destination tuple to the
|
---|
85 | corresponding element in the source tuple. Both tuples must be the same size
|
---|
86 | and the elements assignment compatible => conversions.
|
---|
87 |
|
---|
88 | [long, int, float] dst;
|
---|
89 | [int, char, double] src = [1, 'a', 300.0];
|
---|
90 | dst = src; // => dst.0 = src.0; dst.1 = src.1; dst.2 = src.1
|
---|
91 |
|
---|
92 | ### Tuple Restructuring
|
---|
93 |
|
---|
94 | Tuples can be restructured as part of overload resolution. Function calls
|
---|
95 | unpack tuples and repack tuples to match signatures. This semantics is a form
|
---|
96 | of implicit conversion and is considered during overload resolution.
|
---|
97 |
|
---|
98 | A simple example is matching multiple parameters of a function to a single
|
---|
99 | argument expression, where each parameter is bound to a different element of
|
---|
100 | the returned tuple.
|
---|
101 |
|
---|
102 | [int, int] argFunc();
|
---|
103 | void parmFunc(int a, int b, int c, int d);
|
---|
104 |
|
---|
105 | parmFunc(argFunc(), argFunc());
|
---|
106 |
|
---|
107 | // Roughly equivilent to:
|
---|
108 | [int, int] fst = argFunc();
|
---|
109 | [int, int] snd = argFunc();
|
---|
110 | parmFunc(fst.0, fst.1, snd.0, snd.1);
|
---|
111 |
|
---|
112 | There are few languages supporting multiple return-values as a standalone
|
---|
113 | feature (SETL). Go has multiple return-values but restricts their use in
|
---|
114 | matching arguments to parameters.
|
---|
115 |
|
---|
116 | func argFunc() (int, int) {
|
---|
117 | return 3, 7
|
---|
118 | }
|
---|
119 | func parmFunc( a int, b int ) {
|
---|
120 | fmt.Println(a, b )
|
---|
121 | }
|
---|
122 | func main() {
|
---|
123 | parmFunc2( argFunc() ); // arguments must match exactly with parameters
|
---|
124 | }
|
---|
125 |
|
---|
126 | ### Tuple Casts
|
---|
127 |
|
---|
128 | C-style casts can be used on tuples. These are usually conversion casts (casts
|
---|
129 | that perform operations on the cast type, as opposed to reinterpreting the
|
---|
130 | existing value).
|
---|
131 |
|
---|
132 | Tuple casts can remove elements from the end of a tuple and apply a recursive
|
---|
133 | cast to any of the elements. As an example:
|
---|
134 |
|
---|
135 | [char, char, char] x = ['a', 'b', 'c'];
|
---|
136 | ([int, char])x;
|
---|
137 |
|
---|
138 | This casts the first element type of x from a char to an int and drops the last
|
---|
139 | element. The second line can be replaced with the following code, which creates
|
---|
140 | a new tuple by directly casting the kept elements of the old tuple:
|
---|
141 |
|
---|
142 | [(int)x.0, (char)x.1];
|
---|
143 |
|
---|
144 | Note, tuple casting is more restricted than the implicit tuple restructuring.
|
---|
145 | It cannot do any restructuring beyond dropping the trailing elements of
|
---|
146 | tuples. For example,
|
---|
147 |
|
---|
148 | int i; char c; bool b;
|
---|
149 | [i, b, c, i] = [i, [b, c], i];
|
---|
150 | [int, [bool, char], int] w;
|
---|
151 | [i, b, c, i] = w;
|
---|
152 | [i, b, c, i] = ([int, bool, char, int])w; // fails
|
---|
153 | [i, b, c] = ([int, [bool, char]])w; // works
|
---|
154 |
|
---|
155 | ### Polymorphic Tuple Parameters
|
---|
156 |
|
---|
157 | One kind of polymorphic parameter is the tuple type parameter, previously
|
---|
158 | noted by the `ttype` keyword and now marked by a tailing ellipses (`...`).
|
---|
159 | Although described as a tuple type, it is usually viewed as its flattened
|
---|
160 | sequence of types.
|
---|
161 |
|
---|
162 | forall( T | { int ?>?( T, T ); } )
|
---|
163 | T max( T v1, T v2 ) { return v1 > v2 ? v1 : v2; } // base case
|
---|
164 |
|
---|
165 | forall(T, Ts... | { T max(T, T); T max(Ts); }) // recursive case
|
---|
166 | T max(T arg, Ts args) {
|
---|
167 | return max(arg, max(args));
|
---|
168 | }
|
---|
169 |
|
---|
170 | This feature introduces a type name into scope (Ts). It is used as a type but
|
---|
171 | because the tuple is flattened, the second assertion "T max(Ts);" matches types
|
---|
172 | with multiple parameters (the `...`), although it is used as a tuple function
|
---|
173 | inside the function body (max(args)).
|
---|
174 |
|
---|
175 | The first non-recursive max function is the polymorphic base-case for the
|
---|
176 | recursion, i.e., find the maximum of two identically typed values with a
|
---|
177 | greater-than (>) operator. The second recursive max function takes two
|
---|
178 | parameters, a T and a Ts tuple, handling all argument lengths greater than two.
|
---|
179 | The recursive function computes the maximum for the first argument and the
|
---|
180 | maximum value of the rest of the tuple. The call of max with one argument is
|
---|
181 | the recursive call, where the tuple is converted into two arguments by taking
|
---|
182 | the first value (lisp car) from the tuple pack as the first argument
|
---|
183 | (flattening) and the remaining pack becomes the second argument (lisp cdr).
|
---|
184 | The recursion stops when the tuple is empty. For example, max( 2, 3, 4 )
|
---|
185 | matches with the recursive function, which performs return max( 2, max( [3, 4]
|
---|
186 | ) ) and one more step yields return max( 2, max( 3, 4 ) ), so the tuple is
|
---|
187 | empty.
|
---|
188 |
|
---|
189 |
|
---|
190 | Issues with the Current State
|
---|
191 | -----------------------------
|
---|
192 | There are a variety of problems with the current implementation which need to
|
---|
193 | be fixed.
|
---|
194 |
|
---|
195 | ### Tuples are not Objects
|
---|
196 |
|
---|
197 | Spoilers: this proposal actually takes them even further away from being
|
---|
198 | objects, but illustrates why the current version is not as useful is it could
|
---|
199 | be.
|
---|
200 |
|
---|
201 | Because of the fluid nature of tuples (flattening/structuring), routines like
|
---|
202 | constructions, destructor, or assignment do not make sense, e.g. this
|
---|
203 | constructor matches multiple a tuple types:
|
---|
204 |
|
---|
205 | void ?{} ( [int, int, int] ? this );
|
---|
206 | [int, [int, int]] x; // all match constructor type by flattening
|
---|
207 | [int, int, int] y;
|
---|
208 | [[int, int], int] z;
|
---|
209 |
|
---|
210 | as could a similarly typed destructor or assignment operator. This prevents
|
---|
211 | tuples from being interwoven with regular polymorphic code.
|
---|
212 |
|
---|
213 | ### Providing TType Arguments is Inconsistent
|
---|
214 |
|
---|
215 | The syntax for ttype arguments is slightly inconsistent. It has not come up
|
---|
216 | much yet, because you do not directly provide ttype polymorphic arguments to
|
---|
217 | functions and there are very few existing use-cases for ttype structures.
|
---|
218 |
|
---|
219 | Passing arguments to a function inlines the arguments
|
---|
220 | while passing them to a polymorphic type requires them to be
|
---|
221 | enclosed in a tuple. Compare `function(x, y, z)` with `Type(A, [B, C])`.
|
---|
222 |
|
---|
223 | This did not come up previously as there was little reason to explicitly
|
---|
224 | provide ttype arguments. They are implicit for functions and there is very
|
---|
225 | little use case for a ttype on a struct or union.
|
---|
226 |
|
---|
227 | ### Syntax Conflict
|
---|
228 |
|
---|
229 | The tuple syntax conflicts with designators and the new C++-style attribute
|
---|
230 | syntax.
|
---|
231 |
|
---|
232 | struct S { int a[10]; } = { [2] = 3 }; // [2] looks like a tuple
|
---|
233 | [[ unused ]] [[3, 4]]; // look ahead problem
|
---|
234 |
|
---|
235 | These conflicts break C compatibility goals of Cforall. Designators had to
|
---|
236 | their syntax change and Cforall cannot parse the new attributes.
|
---|
237 |
|
---|
238 | Although most of this redesign is about the semantics of tuples, but an update
|
---|
239 | to tuple syntax that removes these conflicts would improve the compatibility of
|
---|
240 | Cforall going forward (and also open up the new attribute syntax for cforall
|
---|
241 | features).
|
---|
242 |
|
---|
243 | Change in Model
|
---|
244 | ---------------
|
---|
245 | This proposal modifies the existing tuples to better handle its main use
|
---|
246 | cases. There are some use cases that are no longer handled, and a new
|
---|
247 | struct tuple is added to cover those cases.
|
---|
248 |
|
---|
249 | The new tuples is even more "unstructured" than before. New tuples are
|
---|
250 | considered a sequence of types or typed entities. These packs are then unpacked
|
---|
251 | into the surrounding context. Put differently, tuples are now flattened as much
|
---|
252 | as possible, with some places (like parameter lists) being treated as an
|
---|
253 | implicit tuple and the tuple being flattened into that.
|
---|
254 |
|
---|
255 | Structured tuples are now a separate feature: a structure called "tuple".
|
---|
256 | These are polymorphic structures; an instance should act as a structure, except
|
---|
257 | its fields are accessed using indices instead of field names. Experience so far
|
---|
258 | is that structured tuples are not used often, but fill in the use cases that
|
---|
259 | unstructured tuples no longer support.
|
---|
260 |
|
---|
261 | Note that the underlying implementation might not actually look like this.
|
---|
262 |
|
---|
263 | Changed Features
|
---|
264 | ----------------
|
---|
265 | Some of the concrete changes to the features of the language.
|
---|
266 |
|
---|
267 | ### Structured Tuple Type
|
---|
268 |
|
---|
269 | There is a standard library or built-in type named `tuple`, it does not need a
|
---|
270 | special syntax to write types or instances. The type definition might need some
|
---|
271 | primitive support, but if supported as a regular type would look something like
|
---|
272 | this:
|
---|
273 |
|
---|
274 | forall(Ts...)
|
---|
275 | struct tuple {
|
---|
276 | inline Ts all; // inline all specified fields
|
---|
277 | };
|
---|
278 |
|
---|
279 | This type is constructed the same way as most types, a list initializer with
|
---|
280 | each tuple argument, and the lifetime functions (construction, assignment and
|
---|
281 | destruction) work the same. Field access works two ways, the first is accessing
|
---|
282 | the `all` field, effectively converting the structured tuple into an
|
---|
283 | unstructured tuple, the other is to use tuple indexing directly on the
|
---|
284 | structure as if it is an unstructured tuple.
|
---|
285 |
|
---|
286 | (If `inline` does not work, just rename all to `get`. It does make things a bit
|
---|
287 | longer but has no change in functionality. If the `all` access does not work,
|
---|
288 | that is more problematic, and tuple slicing might provide a work around.)
|
---|
289 |
|
---|
290 | ### Type Qualifier Distribution
|
---|
291 |
|
---|
292 | Because tuples are no longer object types, applying type modifiers to them,
|
---|
293 | such as cv-qualifiers and pointers, no longer makes sense. That syntax is
|
---|
294 | now considered distribution, applying the type modifier to each element of
|
---|
295 | the tuple.
|
---|
296 |
|
---|
297 | Previously `const [int, bool] &` would mean a const reference to a tuple of an
|
---|
298 | integer and a boolean. Now it means an alias for `[const int &, const bool &]`,
|
---|
299 | a tuple of a reference to a constant integer and a reference to a constant
|
---|
300 | boolean. This also applies to polymorphic tuple type packs `Ts &` in
|
---|
301 | polymorphic functions.
|
---|
302 |
|
---|
303 | This new approach can specify restrictions on tuple variables as for a single
|
---|
304 | type variable. For example, this approach can replace the special cased tuple
|
---|
305 | operations multi-assignment (N-to-N) and mass-assignment (1-to-N).
|
---|
306 |
|
---|
307 | // Multi-Assignment
|
---|
308 | forall(T, Us... |
|
---|
309 | { T & ?=?(T &, T const &); Us & ?=?(Us &, Us const &); })
|
---|
310 | [T, Us] & ?=?(T & dst, Us & dsts, T const & src, Us const & srcs) {
|
---|
311 | dst = src;
|
---|
312 | dsts = srcs;
|
---|
313 | return [dst, dsts];
|
---|
314 | }
|
---|
315 |
|
---|
316 | // Mass-Assignment
|
---|
317 | forall(T, U, Vs... |
|
---|
318 | { U & ?=?(U &, T const &); Vs & ?=?(Vs &, T const &); })
|
---|
319 | [U, Vs] & ?=?(U & dst, Vs & dsts, T const & src) {
|
---|
320 | dst = src;
|
---|
321 | dsts = src;
|
---|
322 | return [dst, dsts];
|
---|
323 | }
|
---|
324 |
|
---|
325 | These may not work exactly as given (for one, the copy assignment assertion
|
---|
326 | in the first function would likely be redundant/conflicting with the implicit
|
---|
327 | assertions on the parameters), but they show the pattern. Multi-assignment
|
---|
328 | also would be very hard to write with simple tuple types, because the
|
---|
329 | relationship between the two halves of the parameter list does have to line
|
---|
330 | up, that cannot be enforced with two different tuples.
|
---|
331 |
|
---|
332 | ### Type Packs
|
---|
333 |
|
---|
334 | This approach is not a new feature, but a reframing/extension of existing tuple
|
---|
335 | tuple polymorphic parameters as polymorphic type packs. The old `Vars...`
|
---|
336 | syntax introduces a pack of types into scope. It can be used in much the same
|
---|
337 | way as a tuple, but in some new ways to.
|
---|
338 |
|
---|
339 | The primary existing use remains: to use a polymorphic pack in a parameter
|
---|
340 | list, both as part of an assertion and in the signature of the main
|
---|
341 | function. The difference is that this is not an enclosed tuple, but a series of
|
---|
342 | types. The only effective difference this makes is it does not prefer to match
|
---|
343 | another tuple/pack.
|
---|
344 |
|
---|
345 | This pattern continues to a parameter defined with a pack of types, which
|
---|
346 | is considered a pack of parameters, and the name it introduces is a pack
|
---|
347 | of variables, or a flattened tuple.
|
---|
348 |
|
---|
349 | forall(Params...)
|
---|
350 | void function(Params values);
|
---|
351 |
|
---|
352 | New use cases include declarations of members and variables. For example, the
|
---|
353 | creation of a structured tuple structure:
|
---|
354 |
|
---|
355 | forall(Fields...)
|
---|
356 | struct tuple {
|
---|
357 | Fields get;
|
---|
358 | };
|
---|
359 |
|
---|
360 | This is again, treated as a pack of members. They have the same layout as
|
---|
361 | if they were written out by hand. Now, the name get is still accessed as if
|
---|
362 | it was a regular, singular, member. The result of that expression is a
|
---|
363 | pack expression, a tuple of all the field accesses, which can be used with a
|
---|
364 | tuple index expression to access the underlying members. For example:
|
---|
365 |
|
---|
366 | tuple(bool, char, int) data = { ... };
|
---|
367 | // This expression:
|
---|
368 | data.get.2;
|
---|
369 | // Is the same as (if the fields had these names):
|
---|
370 | data.[__anonymous0, __anonymous1, __anonymous2].2;
|
---|
371 |
|
---|
372 | For local declarations it works similarly, except the name introduced is
|
---|
373 | directly usable as a tuple.
|
---|
374 |
|
---|
375 | forall(Objects...)
|
---|
376 | void function( ??? ) {
|
---|
377 | ???
|
---|
378 | Objects objs = ???;
|
---|
379 | ???
|
---|
380 | }
|
---|
381 |
|
---|
382 | ### Tuple Declaration/Deconstruction
|
---|
383 |
|
---|
384 | Declaring a tuple acts as a pack of variable declarations. When this is done
|
---|
385 | with a written out type (as opposed to a polymorphic parameter above), then the
|
---|
386 | elements of the tuple can be named.
|
---|
387 |
|
---|
388 | [int quo, int rem] ret = divmod(a, b);
|
---|
389 |
|
---|
390 | Here `ret` refers to the tuple, the entire pack, while `quo` and `rem`
|
---|
391 | give explicit names to elements of the tuple. Not all the names have to be
|
---|
392 | provided, at the very least, any element name can be omitted if the pack name
|
---|
393 | is provided, and the pack name can be omitted if all the element names are
|
---|
394 | provided. That would ensure every element can be accessed, but it could be
|
---|
395 | reduced even more, effectively immediately dropping some values if there is
|
---|
396 | no named to access it.
|
---|
397 |
|
---|
398 | PAB: I do understand the point of this.
|
---|
399 |
|
---|
400 | ### Tuple Casts
|
---|
401 |
|
---|
402 | Tuple casts are no longer allowed to do any restructuring. Internal
|
---|
403 | restructuring would not be useful, as there is no internal structure.
|
---|
404 | Dropping tail elements could be added back in but it is a narrow use case
|
---|
405 | so it may be replaced with other features (for example: tuple slicing).
|
---|
406 |
|
---|
407 | ### Forbidden Tuples
|
---|
408 |
|
---|
409 | The unstructured tuple cannot represent all the types that the previous
|
---|
410 | semi-structured tuple could. These cases still exist in various ways,
|
---|
411 | specifically in the internals of a polymorphic type, but in general should be
|
---|
412 | considered in their reduced form.
|
---|
413 |
|
---|
414 | Forbidding some of these tuples may remove ambiguity and solve some syntax
|
---|
415 | conflicts that currently exist.
|
---|
416 |
|
---|
417 | Nullary, or 0 element tuples, are equivalent to void, the type that carries
|
---|
418 | no data, because they do not either. It should either be replaced with void
|
---|
419 | or removed entirely when it appears in a larger sequence.
|
---|
420 |
|
---|
421 | // For example, this function:
|
---|
422 | forall(Ts...) Ts example(int first, ????)
|
---|
423 | // With Ts = [], should not be treated as:
|
---|
424 | [] example(int first, [] middle, int last);
|
---|
425 | // But instead:
|
---|
426 | void example(int first, int last);
|
---|
427 |
|
---|
428 | Unary, or 1 element tuples, should be the same as their element type. That
|
---|
429 | is to say a single type in an unstructured tuple is a no-op.
|
---|
430 |
|
---|
431 | Lastly, nested tuples are always flattened to a one-depth tuple. This means
|
---|
432 | that `[bool, [char, int], float]` is resolved as `[bool, char, int, float]`,
|
---|
433 | with the internal structure of the tuple ignored.
|
---|
434 |
|
---|
435 | The flatten into a large sequence rule mentioned above is actually just an
|
---|
436 | application of this. Unstructured tuples can already be restructured, even at
|
---|
437 | the top level of an function call. This can be expressed by considering the
|
---|
438 | argument list as a tuple:
|
---|
439 |
|
---|
440 | call(false, ['a', -7], 3.14)
|
---|
441 | call([false, ['a', -7], 3.14])
|
---|
442 | call([false, 'a', -7, 3.14])
|
---|
443 | call(false, 'a', -7, 3.14)
|
---|
444 |
|
---|
445 | The ability to write nested tuples may remain so tuple deconstruction can be
|
---|
446 | used to name a slice of the tuple.
|
---|
447 |
|
---|
448 | ### Tuple Slicing (Range Tuple Indexing)
|
---|
449 |
|
---|
450 | (Disclaimer: this is a bit more impulsive, see end for notes.)
|
---|
451 |
|
---|
452 | This is an extension to tuple indexing. Currently, only single location of a
|
---|
453 | tuple can be index, extracting the element at that location. By extending the
|
---|
454 | index expression to be a list-range a multiple sub-tuples can be extracted.
|
---|
455 |
|
---|
456 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].0~2,5~9~2
|
---|
457 |
|
---|
458 | produces the tuple
|
---|
459 |
|
---|
460 | [0, 1, 2, 5, 7, 9]
|
---|
461 |
|
---|
462 | (Note the position and values are the same to simplify the example.) That is,
|
---|
463 | index the first 3 tuple elements, and then indexes elements 5 to 9 in steps of
|
---|
464 | 2. Not all selections are possible with this mechanism (e.g., index the
|
---|
465 | Fibonacci elements), but it covers many cases.
|
---|
466 |
|
---|
467 | To get the new tuple, the range has to be constructed and traversed at compile
|
---|
468 | time. The size of the range is the size of the result tuple, with each element
|
---|
469 | in the result tuple decided by the matching element of the range, which gives
|
---|
470 | the index of the original tuple to place there. The type of the indices may be
|
---|
471 | an integral type, but all indices must be in range, otherwise it is a compile
|
---|
472 | time error.
|
---|
473 |
|
---|
474 | In terms of the existing range loops, if you could write this dynamically, it
|
---|
475 | would look something like this:
|
---|
476 |
|
---|
477 | // Implementation of: output = input.RANGE
|
---|
478 | dyn output = []
|
---|
479 | for ( index : RANGE ) { output = [output, input.index] }
|
---|
480 |
|
---|
481 | The result of the expression is only has to be considered a tuple if the
|
---|
482 | range has two or more values, otherwise it can be considered void or the same
|
---|
483 | as indexing the single value in the range.
|
---|
484 |
|
---|
485 | Some closing notes, this is dependent on generalized range expressions.
|
---|
486 | The iterators proposal has a section on some new range types, this feature
|
---|
487 | is intended to be built on that feature. Not simply reuse some of the syntax
|
---|
488 | that is also used in the special for loop. And compile-time evaluation may
|
---|
489 | need to be improved.
|
---|
490 |
|
---|
491 | PAB, I don't understand this last part as the index range is compile time not
|
---|
492 | runtime.
|
---|
493 |
|
---|
494 | Implementation
|
---|
495 | --------------
|
---|
496 | An overview of the implementation details of the new proposal.
|
---|
497 |
|
---|
498 | ### Structured Tuple Implementation
|
---|
499 |
|
---|
500 | Under the hood, unstructured tuples are implemented as structured tuples, with
|
---|
501 | the restructuring code inserted wherever needed. In short, the base
|
---|
502 | implementation should stay mostly the same.
|
---|
503 |
|
---|
504 | PAB: The current implementation does not use convert unstructured tuples to
|
---|
505 | structured tuples. Look at the code generated for
|
---|
506 |
|
---|
507 | int x, y;
|
---|
508 | [x, y] = 3;
|
---|
509 | [x, y] = [y, x];
|
---|
510 |
|
---|
511 |
|
---|
512 | Actually inlining tuples can be done in some cases, it may even help with
|
---|
513 | some forms like a fixed tuple decomposition. However, polymorphic tuples
|
---|
514 | still need to be reduced to a single generic form, which would be a
|
---|
515 | polymorphic container, a tuple.
|
---|
516 |
|
---|
517 | ### AST Updates
|
---|
518 |
|
---|
519 | The current AST cannot represent all the new features. Particularly, an
|
---|
520 | object declaration cannot name elements of the tuple. To this end a new
|
---|
521 | node type, `TupleDecl`, should be added to handle tuple deconstruction
|
---|
522 | declarations (other cases can still be handled with `ObjectDecl`).
|
---|
523 |
|
---|
524 | This would act much like a `FunctionDecl` except for tuples, narrowing the
|
---|
525 | possible types, to `TupleType` instances instead of `FunctionType` instances,
|
---|
526 | and storing some additional information. In this case, the names of the
|
---|
527 | elements of the tuples.
|
---|
528 |
|
---|
529 | PAB: the following parses:
|
---|
530 |
|
---|
531 | [int x, int y] foo( int p );
|
---|
532 |
|
---|
533 | and discussed by Till.
|
---|
534 |
|
---|
535 | (I'm not actually going to decide the implementation now, but some early
|
---|
536 | examination of the problem suggests that it might be better off wrapping a
|
---|
537 | series of `ObjectDecl` rather than just some strings to hold the names.)
|
---|
538 |
|
---|
539 | ### Field Packs
|
---|
540 |
|
---|
541 | Field packs in structures probably have to be written out in full by the
|
---|
542 | specialization pass. If not, it could have some negative effects on layout,
|
---|
543 | causing a structure to take up extra space. It may be able to reuse some of the
|
---|
544 | specialization code for the existing tuples.
|
---|
545 |
|
---|
546 | Related Features in Other Languages
|
---|
547 | -----------------------------------
|
---|
548 | Other languages have similar features. Organized by the related feature.
|
---|
549 |
|
---|
550 | (In hindsight, I may have gone overboard with the number of examples.)
|
---|
551 |
|
---|
552 | ### Structured Tuples
|
---|
553 |
|
---|
554 | There are many languages with structured tuples. Usually just called tuples,
|
---|
555 | but they usually follow the same rules as a polymorphic anonymous structures,
|
---|
556 | that is to say N types are provided to create a new N-arity tuple. They also
|
---|
557 | usually have some special syntax to index the tuples, because there are no
|
---|
558 | field names.
|
---|
559 |
|
---|
560 | #### Rust
|
---|
561 |
|
---|
562 | Rust has the standard tuples as a primitive in the language. Each arity of
|
---|
563 | tuple is a different polymorphic type.
|
---|
564 |
|
---|
565 | Tuple types and expressions are written with a parenthesized, comma separated
|
---|
566 | list of types or expressions. To avoid confusion with the grouping "(...)",
|
---|
567 | one-element tuples must have a trailing comma (and larger tuples may have a
|
---|
568 | trailing comma).
|
---|
569 |
|
---|
570 | const empty: () = ();
|
---|
571 | const single: (usize,) = (12,)
|
---|
572 | const double: (usize, isize) = (34, -56);
|
---|
573 |
|
---|
574 | Element access uses similar syntax to field access (that is "."), but uses an
|
---|
575 | integer literal instead of a field name (for example: "pair.1"). Tuples
|
---|
576 | support pattern matching with similar syntax to their expression form.
|
---|
577 |
|
---|
578 | Some tuple features only apply up to 12-arity tuples.
|
---|
579 | It is not directly stated, but I believe the difference in the more limited
|
---|
580 | features are implemented in the standard library, one for each arity of tuple.
|
---|
581 |
|
---|
582 | - https://doc.rust-lang.org/std/primitive.tuple.html
|
---|
583 | - https://doc.rust-lang.org/reference/types/tuple.html
|
---|
584 | - https://doc.rust-lang.org/reference/expressions/tuple-expr.html
|
---|
585 |
|
---|
586 | #### C++ tuple
|
---|
587 |
|
---|
588 | Implemented as a template type defined in the standard library. No special
|
---|
589 | language features exist to support this, due to the power of C++'s template
|
---|
590 | meta-programming.
|
---|
591 |
|
---|
592 | C++ is also one of the few languages with support for variadic polymorphic
|
---|
593 | types, so there is one template that defines all the types. It is written
|
---|
594 | as `std::tuple<TYPE...>`, where "TYPE..." is any number of comma separated
|
---|
595 | types. This is the standard notation for template type instances.
|
---|
596 |
|
---|
597 | There is no special syntax for member access of a tuple. A template function is
|
---|
598 | used, e.g., `std::get<0>( tuple )`.
|
---|
599 |
|
---|
600 | C++ also has structured binding, a kind of limited pattern matching. In a
|
---|
601 | structured binding declaration, you can write an auto typed declaration with
|
---|
602 | a list of identifiers in a `[]` list.
|
---|
603 | For example, `auto [first, second] = getSize2Tuple();`.
|
---|
604 |
|
---|
605 | - https://en.cppreference.com/w/cpp/utility/tuple
|
---|
606 | - https://en.cppreference.com/w/cpp/language/structured_binding
|
---|
607 |
|
---|
608 | PAB: I do not understand the syntax `auto [first, second]`. Where does it come
|
---|
609 | from?
|
---|
610 |
|
---|
611 | #### C++ template
|
---|
612 |
|
---|
613 | C++ templates can take various types of parameters, including parameter
|
---|
614 | packs. These contain series of values. These are used in pack expansion,
|
---|
615 | which usually expand to a comma separated list, but it can also be a chain of
|
---|
616 | boolean binary operator applications. For example, if the parameter
|
---|
617 | `typename... Ts` is in scope, then you can declare `std::tuple<Ts...>` to
|
---|
618 | introduce a tuple type, if Ts = bool, char, int then the type becomes
|
---|
619 | `std::tuple<bool, char, int>`.
|
---|
620 |
|
---|
621 | A common use is for perfect argument forwarding, which shows some different
|
---|
622 | uses of the pattern:
|
---|
623 |
|
---|
624 | ```
|
---|
625 | template<typename inner_t>
|
---|
626 | class Outer {
|
---|
627 | inner_t inner;
|
---|
628 | public:
|
---|
629 | template<typename... Args>
|
---|
630 | Outer(Args&&... args) : inner(std::forward<Args>(args)...) {}
|
---|
631 | };
|
---|
632 | ```
|
---|
633 |
|
---|
634 | In the first application, `Args&&... args` both uses a pack and introduces
|
---|
635 | another one. `Arg0&& arg0, Arg1&& arg1, Arg2&& arg2, ...` is the pattern
|
---|
636 | it expands too. The `&&` is used to copy the argument type's reference
|
---|
637 | qualifier (the default can strip references away).
|
---|
638 |
|
---|
639 | The second application, `std::forward<Args>(args)...` uses two packs. These
|
---|
640 | are expanded in parallel, and must be the same length (in this case they
|
---|
641 | always will be). It also helps show that the `...` is actually the bit doing
|
---|
642 | the expansion, it is a suffix "operator" to the expansion pattern.
|
---|
643 |
|
---|
644 | There are also fold expressions that use binary operators to combine a pack
|
---|
645 | into a single expression. For example, `args + ... + 0` which adds every
|
---|
646 | element of the `args` pack together.
|
---|
647 |
|
---|
648 | C++ is about the best you could ask for in this area, but it does a lot of work
|
---|
649 | at compile time to make this happen.
|
---|
650 |
|
---|
651 | - https://en.cppreference.com/w/cpp/language/template_parameters
|
---|
652 | - https://en.cppreference.com/w/cpp/language/parameter_pack
|
---|
653 | - https://en.cppreference.com/w/cpp/language/fold
|
---|
654 |
|
---|
655 | #### Haskell
|
---|
656 |
|
---|
657 | Haskell has a special syntax for tuples, but otherwise they are completely
|
---|
658 | normal polymorphic types. Because of this, tuples have a maximum size.
|
---|
659 | Haskell (98) supports tuples of up to 15 elements and the standard library
|
---|
660 | has functions for tuples of up to 7 elements.
|
---|
661 |
|
---|
662 | The syntax for tuples is a comma separated list of elements. Either element
|
---|
663 | types to create a tuple type, or element values to create a tuple value. The
|
---|
664 | context decides among them, such as `(6, "six")` or `(Bool, Char, Int)`.
|
---|
665 |
|
---|
666 | Also all the elements can be removed, getting an expression like "(,)" or
|
---|
667 | "(,,,)", which can be then be used as a function, for a type, or an expression.
|
---|
668 |
|
---|
669 | Haskell supports pattern matching as the main way to extract values from a
|
---|
670 | tuple, although helper functions "fst" and "snd" are provided for field
|
---|
671 | access on two element tuples.
|
---|
672 |
|
---|
673 | Haskell does not have 0 or 1-element tuples. The nil type, written "()" for
|
---|
674 | both type and value, is effectively the 0 element tuple. There is also a type
|
---|
675 | called "Solo" that is a polymorphic structure with one field and is used as
|
---|
676 | the 1-element tuple, but has regular Haskell data-type syntax.
|
---|
677 |
|
---|
678 | - https://www.haskell.org/onlinereport/basic.html
|
---|
679 |
|
---|
680 | #### OCaml
|
---|
681 |
|
---|
682 | OCaml only supports multi-element (2 or more) tuples. It does have the `unit`
|
---|
683 | type, which has one value, written `()`. Tuple types are written as a '*'
|
---|
684 | separated list of types, tuple values are written as ',' separated list of
|
---|
685 | expressions. Pattern matching tuples is supported and uses the same syntax as
|
---|
686 | values. Parenthesizing these lists is only needed for grouping.
|
---|
687 |
|
---|
688 | - https://ocaml.org/docs/basic-data-types#tuples
|
---|
689 |
|
---|
690 | #### Swift
|
---|
691 |
|
---|
692 | Swift has tuple types that use the basic parenthesized, comma separated list of
|
---|
693 | types or values. It only supports 0 and 2 or more element tuples (the `Void`
|
---|
694 | type is an alias for the empty tuple type).
|
---|
695 |
|
---|
696 | Swift also supports named tuples. Names can be added before the tuple element,
|
---|
697 | both for the tuple type and value. The syntax is a name followed by a colon,
|
---|
698 | e.g., `(first: int, second: int)`. These names are a fixed part of the type,
|
---|
699 | and can be used as part of field access notation (otherwise numbers are used
|
---|
700 | in-place of field names `tuple.0` vs. `tuple.first`).
|
---|
701 |
|
---|
702 | - https://docs.swift.org/swift-book/documentation/the-swift-programming-language/types/#Tuple-Type
|
---|
703 |
|
---|
704 | #### Python
|
---|
705 |
|
---|
706 | In Python tuples are immutable lists. Because they are dynamically typed,
|
---|
707 | there is only one tuple type `tuple`.
|
---|
708 |
|
---|
709 | It also has various named tuples. The first, namedtuple, allows naming the
|
---|
710 | elements of the tuple. The second, NamedTuple, is actually a way of creating a
|
---|
711 | typed record in a normally untyped language.
|
---|
712 |
|
---|
713 | - https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range
|
---|
714 | - https://docs.python.org/3/library/collections.html#collections.namedtuple
|
---|
715 | - https://docs.python.org/3/library/typing.html#typing.NamedTuple
|
---|
716 |
|
---|
717 | #### LISP
|
---|
718 | As LISP is dynamically typed, its `cons` data type is an untyped pair and is
|
---|
719 | (perhaps infamously) the main constructor of all compound data types. The
|
---|
720 | function `cons` takes two arguments and builds a pair. Functions `car` and
|
---|
721 | `cdr` get the first and second elements of the pair.
|
---|
722 |
|
---|
723 | ### Packs
|
---|
724 | Packs (or unstructured tuples) are a much less common feature. In fact, there
|
---|
725 | might just be one language, C++, that supports packs. The most common use
|
---|
726 | case for unstructured tuples is returning multiple values, so there is a
|
---|
727 | comparison to languages that have that as a special feature.
|
---|
728 |
|
---|
729 | #### Go
|
---|
730 | Go does not have built in tuple types, but it has multi-return syntax that
|
---|
731 | looks like the tuple syntax of many other languages.
|
---|
732 |
|
---|
733 | ```
|
---|
734 | func main() {
|
---|
735 | i, j := returnIntInt()
|
---|
736 | ...
|
---|
737 | }
|
---|
738 |
|
---|
739 | func returnIntInt() (int, int) {
|
---|
740 | return 12, 34
|
---|
741 | }
|
---|
742 | ```
|
---|
743 |
|
---|
744 | - https://golangdocs.com/functions-in-golang
|
---|
745 | - https://go.dev/src/go/types/tuple.go
|
---|
746 |
|
---|
747 | #### Lua
|
---|
748 | Lua is a scripting language that is dynamically typed and stack based. Although
|
---|
749 | the stack is usually only directly visible in the C-API, it does allow any
|
---|
750 | function to return any number of values, even a single return, in the return
|
---|
751 | expression
|
---|
752 |
|
---|
753 | ```
|
---|
754 | local funcion f()
|
---|
755 | return 12, 34
|
---|
756 | end
|
---|
757 |
|
---|
758 | local i, j = f()
|
---|
759 | ```
|
---|