-
Notifications
You must be signed in to change notification settings - Fork 205
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
Comments
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. For records there are two ways the name can be used:
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. 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. 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. |
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); |
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.
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...) |
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.
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.
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.
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. |
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. |
Actually, after thinking about this more on the drive home, maybe I closed it prematurely. :) |
Given the declaration
you can access
v.$1
andv.$2
, but I would have expected to be able to accessv.a
andv.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.:@mraleph stated in another issue (which I branched this separate issue out from):
The text was updated successfully, but these errors were encountered: