Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace record fields ($1, $2, etc.) with named fields if type contains field names (eg. (int a, int b)) #3487

Open
lukehutch opened this issue Nov 29, 2023 · 28 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@lukehutch
Copy link

lukehutch commented Nov 29, 2023

Given the declaration

(int a, int b) v = (1, 2);

you can access v.$1 and v.$2, but I would have expected to be able to access v.a and v.b (these fields don't exist).

I would expect $1 and $2 to not be visible or usable for the type (int a, int b), since there shouldn't be two ways to access the same value.

I would only expect to have to use $1 and $2 if the fields were not named, e.g.:

(int, int) v = (1, 2);

@mraleph stated in another issue (which I branched this separate issue out from):

The choice of names not having any meaning was deliberate, but I don't see any reason why it would not work to just allow this in a statically typed context (i.e. ignoring dynamic invocations entirely).

@lukehutch lukehutch added the feature Proposed language feature that solves one or more problems label Nov 29, 2023
@lrhn
Copy link
Member

lrhn commented Nov 29, 2023

Static names make sense in arguments and records (not surprisingly since they are closely related).

The idea is that a static type with positional entries, either the static parameter list of a function type or the static record type, can contain names for the positional entries, purely for documentationation purposes today, so we might as well use those names for something practical.

For argument lists, we could allow you to pass a positional argument by name.

int limit(int value, int min, int max) => ...
... limit(x, max: 100, min: 0)...

The effect is exactly the same as passing the same value positionally, all it gives you is control over evaluation order, and documentation.
(Unless we get creative, it'll be an error to pass an optional positional argument by name, and not also pass every optional positional argument before it.)

For records there are two ways the name can be used:

  • destructuring, access record.name, what is just a shorthand for record.$n for some n. (And would otherwise be an error).
  • creation, (y: 1, x: 2) with context type (int x, int y) would be a "shorthand" for (2, 1).

Those are both a little more problematic than the argument list.

The destructuring because it can conflict with extension members, in which case the extension would win.

The creation because the syntax is already valid, and has its meaning changed by the context type.
Presumably the unchanged code would be a type error, but that's only if all context types are strong requirements.

The general argument against doing anything like this is that it makes changing the name of a positional parameter or record field a breaking change.
So far, those names have been entirely decorative, for documentation purposes only. If changing them can make code in other libraries break, then they are as much part of the API contract as named parameter names. (You can still change the name in a subclass, it's only per static type.)
That puts a perverse incentive on library writers, where they may want to avoid giving their positional parameters or record field a name at all.

I think I'd suggest that positional names from other libraries are not inferred into the current library. If you need a type with names, you have to write it yourself, so it's a shorthand that only works within a single library.
Then you'll immediately know if you broke something.

@munificent
Copy link
Member

munificent commented Dec 1, 2023

I definitely understand the desire for this feature. It seems like the names are right there and we should be able to hang useful behavior off them.

But my feeling is that doing so would be very brittle and fail in really confusing ways because the names, fundamentally, are not part of the static type. The language spends many pages of the spec precisely defining static types and specifying how they flow through programs. The whole language rests on that. Once we start having behavior that rests on static properties of some code that aren't part of the type, we either have to recapitulate all of that complexity, accept that the behavior won't feel as seamless as other features, or go all the way and actually make these static properties part of the type.

For example, we could try to make this work:

test() {
  (int a, int b) v1 = (1, 2);
  print(v1.a);

  (int x, int y) v2 = (3, 4);
  print(v2.x);
}

But what happens when a user tries to do:

test(bool condition) {
  (int a, int b) v1 = (1, 2);
  (int x, int y) v2 = (3, 4);
  print((condition ? v1 : v2).a); // ?
  print((condition ? v1 : v2).x); // ?
}

Or:

T identity<T>(T value) => value;

test() {
  (int a, int b) v1 = (1, 2);
  var v2 = identity(v1);
  print(v2.a); // Does this still work?
}

Or:

(T1, T2) identityPair<T1, T2>((T1, T2) value) => value;

test() {
  (int a, int b) v1 = (1, 2);
  var v2 = identityPair(v1);
  print(v2.a); // Does this still work?
}

Or:

(T1, T2) identityPair<T1, T2>(T1 value1, T2 value2) => (value1, value2);

test() {
  (int a, int b) v1 = (1, 2);
  var v2 = identityPair(v1.a, v1.b);
  print(v2.a); // Does this still work?
}

Examples like this are rife. The problem is that static types are basically always flowing through a program between where they appear and where they're used. So unless we figure out precisely how these field names would flow through all of that, they'll probably get dropped on the floor so often that they aren't actually useful.

And if we do try to make the field names flow through types, then we'll quickly run into problems like the conditional one here where now two record types that used to be identical now may or may not be (or we have to drop the names on the floor at that point).

Overall, my suspicion is that's just not worth it. If you want named fields, that's what named fields are for. That's why we added them! :)

You can just do:

({int a, int b}) v = (a: 1, b: 2);
print(v.a)
print(v.b);

@lukehutch
Copy link
Author

For example, we could try to make this work:

test() {
  (int a, int b) v1 = (1, 2);
  print(v1.a);

  (int x, int y) v2 = (3, 4);
  print(v2.x);
}

But what happens when a user tries to do:

test(bool condition) {
  (int a, int b) v1 = (1, 2);
  (int x, int y) v2 = (3, 4);
  print((condition ? v1 : v2).a); // ?
  print((condition ? v1 : v2).x); // ?
}

This doesn't work either, because Dart doesn't have duck-typing:

class A {
  final int a;
  final int b;

  A({required this.a, required this.b});
}

class B {
  final int a;
  final int b;

  B({required this.a, required this.b});
}

void test(bool cond, A a, B b) {
  final c = cond ? a : b;
  print(c.a);
}

However, I was in fact thinking assuming the variable names were part of the type of a record. Duck-typing has always made the most intuitive sense to me, although I am aware that this has ramifications for enforcing common storage layouts to ensure speed of execution, etc.

Overall, my suspicion is that's just not worth it. If you want named fields, that's what named fields are for. That's why we added them! :)

You can just do:

({int a, int b}) v = (a: 1, b: 2);
print(v.a)
print(v.b);

That's fair, although the type declarations can become gnarly with this syntax (especially when you have nested records, which I have used a couple of times...)

@munificent
Copy link
Member

But what happens when a user tries to do:

test(bool condition) {
  (int a, int b) v1 = (1, 2);
  (int x, int y) v2 = (3, 4);
  print((condition ? v1 : v2).a); // ?
  print((condition ? v1 : v2).x); // ?
}

This doesn't work either, because Dart doesn't have duck-typing.

That doesn't work, but this does:

test(bool condition) {
  (int a, int b) v1 = (1, 2);
  (int x, int y) v2 = (3, 4);
  print((condition ? v1 : v2).$1); // 1 or 3
  print((condition ? v1 : v2).$2); // 2 or 4
}

Records are structurally typed (i.e. "duck typed") and positional fields only need to have matching types for two records to have the same type.

However, I was in fact thinking assuming the variable names were part of the type of a record.

They are for the named fields in a record, but not the positional fields:

(int a, int b) pair1 = (1, 2);
(int x, int y) pair2 = pair1; // OK.

({int a, int b}) namedPair1 = (a: 1, b: 2);
({int x, int y}) namedPair2 = namedPair1; // Error.

Duck-typing has always made the most intuitive sense to me, although I am aware that this has ramifications for enforcing common storage layouts to ensure speed of execution, etc.

Yeah, records were carefully designed so that even though they are structurally typed, the compiler should always be able to compile field accesses on them to something fairly efficient.

Overall, my suspicion is that's just not worth it. If you want named fields, that's what named fields are for. That's why we added them! :)
You can just do:

({int a, int b}) v = (a: 1, b: 2);
print(v.a)
print(v.b);

That's fair, although the type declarations can become gnarly with this syntax (especially when you have nested records, which I have used a couple of times...)

My general rule for records is that if I find a record type declaration is getting too verbose, that's probably a signal that it's time to make an actual class and give it a real nominal type.

@munificent
Copy link
Member

munificent commented Dec 5, 2023

I'm going to go ahead and close the issue because this is working as intended, but thank you for bringing this up to discuss.

@munificent munificent closed this as not planned Won't fix, can't repro, duplicate, stale Dec 5, 2023
@munificent
Copy link
Member

Actually, after thinking about this more on the drive home, maybe I closed it prematurely. :)

@stephane-archer
Copy link

stephane-archer commented Nov 26, 2024

Seen (int a, int b) v = (1, 2); I would have never guessed without autocomplete that a is accessible with v.$1 and not v.a
Furthermore, if you never use bash before this syntax is so unintuitive. The first argument is not .$0 like you would expect if you look at the rest of the language and the $ sign is usually useful in Dart inside strings, which even had more to the confusion.

While I expect a lot of us to know bash I don't think it is the best source of inspiration for a language and be beginner friendly especially when looking at the rest of the language and considering Dart could be a first language for someone.

Been able to access .$1 with .name as syntax sugar is back compatible with the existing bash style syntax and user-friendly so I don't see any reason to not add it.

@mateusfccp
Copy link
Contributor

@stephane-archer About the index question, see #2638.

@Levi-Lesches
Copy link

Levi-Lesches commented Nov 26, 2024

Also, to avoid the reader of your code making this mistake, I'd recommend not using names at all, or restructuring:

(int, int) getNums() => (1, 2);

// instead of:
final (int a, int b) v = getNums();

// try one of these:
final (int, int) v = getNums();
final v = getNums();
final (a, b) = getNums();

In my opinion, giving names in contexts where they can never be used like this should not have been allowed, as it leads to confusion about why they cannot be used. Or at least, records that are given names in the context of a variable should have their members be accessible by those names:

final (int, int) v = getNums();    // print(v.$1)
final v = getNums();               // print(v.$1)
final (int a, int b) = getNums();  // print(v.a)

@munificent would that work or be too confusing?

@stephane-archer
Copy link

stephane-archer commented Nov 26, 2024

I completely agree with:

final (int, int) v = getNums();    // print(v.$1)
final v = getNums();               // print(v.$1)
final (int a, int b) = getNums();  // print(v.a)

I really want to name my field access because .$5 is so error-prone over .color

Also regarding typedef

typedef Hello = (int height, int with, int scale, int somethingElse)

void foo(Hello h) {
    h.somethingElse; // is so much better than h.$4 
}

@Levi-Lesches
Copy link

Levi-Lesches commented Nov 26, 2024

In cases where you have control over the record definition, use curly braces to indicate real named fields:

typedef MyRecord = (int unnamed, {int named});

void foo(MyRecord record) => print("With name ${record.named}, without name ${record.$1}");

(Also, for future reference, you need three backticks (`) to make a code block, not single quotes (')

@stephane-archer
Copy link

stephane-archer commented Nov 26, 2024

typedef Hello = ({int height, int with, int scale, int somethingElse})

solve a large part of the issue because I can use hello.somethingElse but it becomes verbose for my user and the existing data doesn't work anymore. (1920, 1080, 1, 42) has to be rewritten to (height: 1920, with: 1080, scale: 1, somethingElse: 42) (you can have a lot of these in your existing codebase). While it's better and less error-prone, I would love (1920, 1080, 1, 42) to be valid too, because of its simplicity and shortness (with a quick fix to make them use field names).

I like the approach short to write but explicit with a quick-fix

But to me, this is the best I found so far because named parameters are so much better than .$5.
I reiterate that I fully agree with @Levi-Lesches on:

final (int, int) v = getNums();    // print(v.$1)
final v = getNums();               // print(v.$1)
final (int a, int b) = getNums();  // print(v.a)

@Levi-Lesches my phone seems to not want me to put 3 ` in a text field... (iPhone)

@stephane-archer
Copy link

stephane-archer commented Nov 26, 2024

another use case:

void foo ((String inputPath, String outputPath) message) {
 print(message.inputPath)
}

@mateusfccp
Copy link
Contributor

another use case:

void foo ((String inputPath, String outputPath) message) {
 print(message.inputPath)
}

This is something completely different and is tracked in #3001.

@TekExplorer
Copy link

TekExplorer commented Nov 26, 2024

I have some concerns; currently (int a, int b) and (int x, int y) are the same type

Would this change affect the types, or is it more akin to extension types, where the types and values are the same, and you just happen to have extra getters?

Kind of like;

extension type AB((int a, int b) ab) {
  int get a => $1;
  int get b => $2;
}
extension type XY((int x, int y) xy) {
  int get x => $1;
  int get y => $2;
}

But then, wouldn't it need to be a type thing in order to be consistent with the language?

So; would AB and XY be unrelated subtypes of (int, int)? Do they introduce subtypes at all? If they don't, where are the members attached to?

Consider extensions; don't named positional fields have to be type based so that you can use them in an extension?

extension on (int a, int b) {
  int get a2 => a * 2;
  int get b2 => b * 2;
}

Or would it just be more of a syntax sugar thing, where referencing the name of a positional variable just compiles down to the actual $i? That would imply that you could do xy.a2; That means it has to be in the type, so how do we handle existing named-but-irrelevant examples in the wild today?

It could still be that (int a, int b) == (int x, int y) anyway, but still contains the extra information for sugar-based member access - you just wouldn't be able to do a type check to differentiate them. (1, 2) is (int a, int b) && is (int x, int y)

@TekExplorer
Copy link

TekExplorer commented Nov 26, 2024

another use case:

void foo ((String inputPath, String outputPath) message) {
 print(message.inputPath)
}

This is something completely different and is tracked in #3001.

Nope. that is not destructuring. notice it says message.inputPath, not inputPath on its own.

@stephane-archer
Copy link

Sorry if not everything is related to this exact issue, I try my best to stay on the subject:

void foo ((String inputPath, String outputPath) message) {
 print(message.$1)
 print(message.$2)
}

versus:

void foo ((String inputPath, String outputPath) message) {
 print(message.inputPath)
 print(message.outputPath)
}

now let's add a middle argument and print it last ;)

void foo ((String inputPath, String simpleChange, String outputPath) message) {
 print(message.$1)
 print(message.$3) // $2 became $3 (tricky to spot on a large code review where there is no change on the line)
 print(message.$2) //What is the position of simpleChange to be able to use it? Best question to ask yourself Friday night before the Christmas weekend
}

versus

void foo ((String inputPath, String simpleChange, String outputPath) message) {
 print(message.inputPath)
 print(message.outputPath)
 print(message.simpleChange) // only the last line has changed
}

I think the .$1 syntax is so bad as soon as you change something, everything explodes (silently if the type is the same)

@lukehutch
Copy link
Author

If the names in a tuple type declaration are not even used currently, what harm is there in allowing the names to be used to access fields within that scope? It won't break anything.

Named parameters could act as an alias for $1, $2, etc., for backwards compatibility. I think the numbered parameters $1, $2 are an abomination, and should actually be removed -- but please at least allow us to avoid using them by using names.

@Mike278
Copy link

Mike278 commented Nov 26, 2024

@stephane-archer All these problems with positional fields are the exact reason why records also support named fields! And the verbosity of named fields is the exact reason why records also support positional fields!

Named fields are clearly a more natural fit for the usecases presented, so I think this comment alludes to the actual problem:

it becomes verbose for my user and the existing data doesn't work anymore. (1920, 1080, 1, 42) has to be rewritten to (height: 1920, with: 1080, scale: 1, somethingElse: 42)
[...]
I would love (1920, 1080, 1, 42) to be valid too, because of its simplicity and shortness

Roughly:

  • When you're producing a record, the surrounding context and the expressions being put into the record often make the name redundant. This is where positional fields are nice.
  • When you're consuming a record, most of that context is gone, so all you have is the name. This is where named fields are nice.

In that case, it probably makes more sense to discuss how to produce records in a concise way, rather than try to add naming semantics to fields which are anonymous by definition.

You can't get shorter than (1920, 1080, 1, 42), so the next best thing would be a helper function newThing(1920, 1080, 1, 42).

In practice you're typically producing a record from variables rather than literals - for example I see lots of code that looks like return (height: height, width: width, scale: 1). In this case, it might be nice if we could omit the label when the variable is the same name - i.e. something like return (:height, :width, scale: 1) (sometimes called field/label punning - I thought there was already an issue for this but I couldn't find it).

In general though, my personal heuristic for when to use positional fields is basically:

  • each position has a different type
  • the type is not a primative type
  • the overall structure is positional by nature
  • the positions are extremely obvious

If any of these aren't true, I normally use a named field.

@stephane-archer
Copy link

@Mike278 I think you summarized the issue:

  • When you're producing a record, the surrounding context and the expressions being put into the record often make the name redundant. This is where positional fields are nice.
  • When you're consuming a record, most of that context is gone, so all you have is the name. This is where named fields are nice.

You want named fields when consuming and usually, positional fields when producing.

typedef Hello = ({int height, int with, int scale, int somethingElse})

should be produced with (1920, 1080, 1, 42) (with named fields being valid too and accessible with a quick fix (height: height, width: width, scale: 1))

and being able to consume with named fields: a.scale

Using named field and making a helper function is the best we have today to achieve this but I will be glad if we can move from helper_function(1920, 1080, 1, 42) to (1920, 1080, 1, 42) for producer

@TekExplorer
Copy link

(1920, 1080, 1, 42) is not especially useful. That is precisely what I would use named fields for.

Positional-named fields are more useful with something like (int x, int y, int z) where destructuring may be useful, but where you may also want to use the record as is. Ie, point.z vs final (x, y, z) = getPoint()

In this case, I may create an extension for that record.

For consumption, it may be useful to have the name for a positional field be used to construct an artificial getter pointing to the right field.

If course, you could make it named, which would certainly work, but it's also a marginally worse API when having 3 values out together should arguably be enough.

You could perhaps use an extension type, but you can't currently "implement" a record type even though you should be able to, and we're also effectively in class territory at this point.

@eernstg
Copy link
Member

eernstg commented Nov 27, 2024

I think it could be useful to consider a basic assumption. Perhaps a record type isn't always the best choice for these situations? Let's just compare it to a class.

I'm assuming primary constructors in a form which is similar to this proposal such that the declaration of the class can be concise (but I'm also showing the longer form that we can use today):

// Consider this recent example.
typedef R = ({int height, int width, int scale, int somethingElse});

// A class doing a similar job.
class const C(int height, int width, int scale, int somethingElse);

/* If we don't have primary constructors we can write it like this.
class C { 
  final int height, width, scale, somethingElse;
  const C(this.height, this.width, this.scale, this.somethingElse);
}
*/

void main() {
  // Producing the record.
  var r = (height: 1920, width: 1080, scale: 1, somethingElse: 42);

  // Producing the class instance.
  var c = C(1920, 1080, 1, 42);

  // Destructuring to use the height and the width.
  {
    var (:height, :width, scale: _, somethingElse: _) = r;
    print(height + width); // or
    print(r.height + r.width);
  }
  {
    var C(:height, :width) = c;
    print(height + width); // or
    print(c.height + c.width);
  }
}

In particular, the class will quite naturally allow for creation of entities using positional parameters and destructuring using names.

It could be argued that the object pattern is inconvenient if the class name is long. However, if we get #4124 then we can use a single character when the type is available as the context type:

class const ClassWithALongerName(int height, int width, int scale, int somethingElse);

void main() {
  var _(:height, :width) = ClassWithALongerName(...);
}

There could be several reasons why the good ol' class can compete with the record type:

The short form where the object is constructed with positional arguments is standard, but we can also declare other constructors if we wish to have a named form as well, or any mixture of named and positional parameters.

The class type is nominal, which means that it won't be confused with other types that are conceptually different just because they use the same names (or types and positions, for positional getters).

The record can only be destructured if all getters are mentioned (#3964 tries to improve on this one, though).

Finally, the class is faster because the record works like a generic class (for instance, it must be assignable to the type ({num height, num width, int scale, Object somethingElse})).

Don't worry, I'm not saying that we shouldn't do anything about this particular corner of the record type feature. ;-) At the same time, I think it may be useful to keep other mechanisms like classes in mind.

@eernstg
Copy link
Member

eernstg commented Nov 27, 2024

Returning to the request in the original posting, note that the ability to use the otherwise-just-documentation names of positional members of a record type corresponds precisely to a set of extension getters:

// Assume that we have this declaration.
(int a, int b) v = (1, 2);

// Assume there's a way to get the following, implicitly.
extension on (int, int) {
  int get a => $1;
  int get b => $2;
}

So, as long as the extension is in scope in exactly the locations where we want it to be in scope, it just works. ;-)

We could use this as a starting point for a feature: The compiler would generate something like the extension declaration shown above, and it would be considered accessible exactly in the case where the receiver is that v.

This doesn't immediately generalize, though: What would we do if the type (int a, int b) is used as anything other than a type annotation?

// Perhaps ... ?
void foo<X extends Map<int, (int a, int b)>>(X x) {
  var theA = x[14].a; // Relies on the implicitly induced extension on that `(int, int)`.
  var thePair = x[14];
  thePair.b; // The inferred type of `thePair` enables the extension, too.
}

I don't know if there is a satisfactory solution to the question about 'where is this extension accessible?', but it could "work" to make it accessible in a superset of the optimal locations. At the end of that path we could just write the extension manually, and live with the fact that it can be used with all records of type (int, int).

@TekExplorer already mentioned extension types here. An extension type would need to be obtained explicitly (e.g., we would use TheExtensionType((2, 3)) rather than (2, 3)).

We could use implicit constructors to avoid the explicit transition "into" the extension type.

extension type const AB._((int, int) _record) {
  implicit AB((int, int) pair) : this._(pair);
  int get a => _record.$1;
  int get b => _record.$2;
}

void main() {
  AB v = (1, 2); // OK, implicitly means `AB v = AB((1, 2));`.
  var sum = v.a + v.b; // OK.
  v.$1; // Error, no such member.
}

@TekExplorer
Copy link

TekExplorer commented Nov 27, 2024

I think that has promise with implicit constructors.

I think we'd just need to be able to "implement <record type>" for the free and easy destructuring, which is not currently allowed, then I think we'd have all the functionality we need.

Then we'd just need to work out an easy way to define them. Probably a macro.

The best solution is to have the compiler make it up for us, but I don't see an easy path to that

@mmcdon20
Copy link

I think that has promise with implicit constructors.

I'm not entirely convinced of this myself. Don't get me wrong, I do think implicit constructors are a good idea, and I would like to see them added to the language, but I don't think it solves this particular problem that well. IMO having to define an extension type with named getters and an implicit constructor just to get more descriptive names than .$1 and .$2 is too much boilerplate to be worth doing in many cases.

I think we'd just need to be able to "implement " for the free and easy destructuring, which is not currently allowed, then I think we'd have all the functionality we need.

I have an existing issue for this exact feature if you are interested. #3839

Then we'd just need to work out an easy way to define them. Probably a macro.

Do macros even have access to the names of the positional fields of records? I don't know the answer to this but I suspect they may not.

@Levi-Lesches
Copy link

This doesn't immediately generalize, though: What would we do if the type (int a, int b) is used as anything other than a type annotation?

I don't know exactly how the analyzer is working here, but I'd say whenever the type of a variable can be traced back to a record type that has positional names, this magic extension should be visible.

So this wouldn't just be true based on the type of record, but rather where the record came from, and would cover local variables, fields, parameters, and type parameters. That is, if the analyzer keeps the names in all these cases and doesn't just reduce it down to (int, int).

@TekExplorer
Copy link

Do macros even have access to the names of the positional fields of records? I don't know the answer to this but I suspect they may not.

Macros arent done yet, and I imagine it would be relatively trivial to add the information, since we already know it exists.

Magic extensions would probably be the most effective solution for this particular issue, but may be complicated to implement.

@lrhn
Copy link
Member

lrhn commented Nov 28, 2024

The way I would formally specify something like this (which we have to do, no amount of hand waving is enough) would be:

  • the static type system contains record types with and without names on positional fields. The type with no such names is the canonical type, but a type with some or all positional fields having names is a mutual subtype of the no-named type (a subtype and supertype).
  • positional fields names can be private names.
  • it's a compile time error of a record type has two fields with the same name.
  • a declaration like (int x, int y) p; had static type (int x, int y), with positional names. In general, if a record type clause contains positional fields names, they are part of the type on the static type system.
  • the Up and Down functions on two records with the same shape is defined as the point-wise Up/Down of the fields. Further, if the two fields have the same accessible name, then that is the name of the field in the result. If only one of the fields has an accessible name, then that is the name in the result. Otherwise the field in the result has no name.
  • any other operation that combines two record types with the same shape into one, does the same thing with names.
  • the runtime type system collapses all those types into the canonical type, and does not distinguish types with the same shape and types at all.
  • the Norm function strips names from positional record fields.The Type object for such types are all the same.
  • finally, a member access of the form o.id where the static type of o is a record type with the _n_th positional field having the name id, has the id member resolve to an access on the $n field getter.

The Devil in those details is fixing all the options on types that have to preserve or combine names, and make sure they do so.

And that all accesses behave the same.
Might have to do something for patterns, not sure what.

If we get record spreads, we'll have to drop names that would cause conflicts.

So we get an infinity of equivalent types in the static type system, just to allow you to write o.id without having to actually make the record named.
And instead of writing var (x, y) = point; where you want to use those fields.

SGTM! I already want the same for positional function arguments, so you can write Point(y: 42, x: 37) instead of Point(37, 42). As usual records and function arguments align, what suggests that the idea had merit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems
Projects
None yet
Development

No branches or pull requests

10 participants