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 · 6 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. :)

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

3 participants