| 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 | ```
|
|---|