-
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
Should there be an expression syntax for accessing positional record fields? #2388
Comments
I would like to come up with a positional field accessor expression syntax, yes. I don't have a design yet and I don't think it's essential so the current proposal just says there isn't one. The proposal initially said each one got a named getter like |
Crazy random idea: what if the syntax for positional field accessors is simply the E.g.: (int, String) a = (3, 'foo');
var b = x[0]; // b has static type `int`
var c = x[1]; // c has static type `String`
var d = x[2]; // Static error: no operator [2] in (int, String)
var e = x[0 + 1]; // Static error: no operator[] in (int, String) The CFE could desugar accesses like Probably a terrible idea, but just thought I'd throw it out there :) |
@stereotype441 I think the positional access syntax should work when the receiver has static type If we decide that we must implement However, I would rather not have that metadata in the compiled program. |
I tend to prefer @munificent's first proposal (using names like This allows the mechanism to be consistent and convenient: With respect to parsing, and comparing with the alternative Comparing with With respect to the practical value of accessing positional components using normal getters: Developers can use Dynamic invocations: It seems likely that we can support dynamic invocations, even if the runtime uses a more compact representation for positional components and their names than they do for named components. There would be other corner cases, for example: It would probably not be possible to introduce a non-trivial |
I've suggested Last I was discussing this, I gravitated towards liking So var r = (42, 37, foo: 87);
print("(${r.0}, ${r.1}, ${r.foo})"); would work. We can make it work for dynamic invocations too, it'll just fail if the target is not a record with that many positional elements. That too is a reason for me to not allow I wouldn't make (We can even, in some hypothetical future, choose to allow classes to declare positional members, named |
I'm inclined to agree with the analysis from @lrhn above. |
I think reserving names like Then you get all the benefits of |
We discussed this in the language meeting today. We agree that some expression syntax for accessing positional fields is important. (In particular, I find Leaf's point that without an expression syntax, accessing the nth field requires a pattern of at least n subpatterns, which can be very verbose to be compelling.) We haven't settled on a syntax. A few options we're considering (most already mentioned here):
|
A topic we haven't discussed yet that I think could affect this decision is code that is polymorphic over tuple arity. Right now, there's no plan to be able to write code that works with a record type and is generic over how many positional fields the record has. I suspect that kind of use case will come up. For example, one of the approaches to handle awaiting records (#2321) is defining a set of core library functions like: Future<(T1, T2)> wait2<T1, T2>(
Future<T1> future1, Future<T2> future2) {
...
}
Future<(T1, T2, T3)> wait3<T1, T2, T3>(
Future<T1> future1, Future<T2> future2, Future<T3> future3) {
...
} Having to define separate functions for each arity up to some arbitrary maximum is pretty tedious. C++ introduced a notion of parameter pack to allow templates to write code that can be more flexibly generic over this kind of boilerplate. We could probably tackle this in Dart just using macros. But if we more graceful support for this kind of code, we might want to support variadic generics and a way to build records and record types out of the corresponding type parameter lists. If we do that, then the code working with those generic records may need to access positional fields in an abstracted way. I think the I'm not sure if this is an important constraint (there are many many open questions of how variadic generics would work), but I wanted to put it out there. |
In #2388 (comment) a syntax allowing runtime introspection is listed as a con. In #2388 (comment) runtime introspection is listed as a potential future enhancement. Do we need to separately figure out where we land on this before choosing a syntax? |
In that comment, I'm not necessarily assuming that some kind of variadic generics would rely on runtime introspection. I would definitely prefer that it get expanded statically at compile time, though that certainly raises lots of questions. It may be that the right answer is to lean on macros for this.
Not necessarily. I think we can pick whatever syntax we need for this and it won't entirely paint us into a corner if we later want to be able to write code that's polymorphic over record arity. |
I personally like
It would also look a little weird to see this, but I can see people getting used to it. extension on (num, num) {
double get distanceToOrigin => math.sqrt($0**2 + $1**2);
} |
All the suggested syntaxes work as selectors, so they can be used with null-aware access ( The The The |
In this case the following code became possible extension on double {
Record call() {
return (foo, (("", 3), 3.14));
}
}
extension on Record {
double operator* (double other) => 3.14;
}
void foo() {
print("foo");
}
main() {
3.14().0();
3.14().1.0 * 1.1;
1.1 * 3.14().1.1;
} |
True, and if we then extend var ipv4 = ip.192.168.0.1; or var time = T.11.13.25.pm; or similar shenanigans. Let's ... not do that then. (Not an entirely new possibility, it just looks bettern than (I probably wouldn't allow extension on (Object?, ((String, int), double)) {
...
} should work) |
I like |
OK, the parsing and readability problems of Another problem with extension RecordSubscript on (int, int) {
int operator [](int index) => 3;
}
main() {
(1, 2)[0];
} Should this print It seems like no one likes |
Would it be possible to use the name of a positional field if a name is provided in the type? (int x, int y) position = (5, 10);
print(position.x); |
I believe the point of having positional fields is to deliberately not expose the names of the fields in an API, similar to how you can't pass positional arguments by their names in function calls. |
If you have a record with type But once those arguments are passed in, you then refer to positional arguments by their names within the definitions of functions. Their names do not effect the signature of the function but having a name to refer to them by is more convenient than referring to them by their position. I don't think using the names for positional fields of records in this way would be that much different. The names would not affect the record's shape, and you would be able to provide whatever names you want for the positional fields. (int latitude, int longitude) getPosition() {
return (5, 10);
}
print(getPosition().latitude); // okay
(int x, int y) position = getPosition(); // providing different names for positional fields is okay
print(position.x); // okay
print(position.latitude); // error
print(position == getPosition()); // true There is the downside that changing the name of a positional field would be a breaking change. Not sure if there are other downsides/potential issues that I am missing. |
No complaints, go with We could, if we wanted to, defined That is, we could act as if the platform libraries exposed an infinite set of unnameable and unhidable extensions, one for each record type (tree-shaken to one for each record-shape which exists in the program), so that for extension $$CantTouchThis$$<R, T> on (R, x: T) {
R get $0 => switch (this) { case (it, x: _) => it; case _ => throw WatError("unreachable"); };
T get x => switch (this) { case (_, x: it) => it; case _ => throw WatError("unreachable"); };
} (But much more efficient, obviously!) Then imperative record destructuring will only work at the static type of a record. I think the only advantage of doing so, is that we won't complicate |
No, the positional field name is not part of the record's type. You could have multiple positional records that have different names for a given positional field and all are considered to have the same type and are freely assignable to each other. That means there's no reliable way to know which position a given field name should correspond to. |
I'd consider this a positive feature. |
* Address a bunch of records issues. - Support constant records. Fix #2337. - Support empty and one-positional-field records. Fix #2386. - Re-add support for positional field getters Fix #2388. - Specify the behavior of `toString()`. Fix #2389. - Disambiguate record types in `on` clauses. Fix #2406. * Clarify the iteration order of fields in `==`. * Copy-edit the sections on const records and canonicalization. There should be no meaningful changes. I just: - Fixed some misspellings. - Used Markdown style consistent with the rest of the doc. - Re-worded things to, I hope, read a little more naturally. - Removed the parenthetical on identical() in a const context because that felt a little too academic. * Leave the order that positional fields are checked in == unspecified. * Clarify that positional fields are not sugar for named fields. Specify the evaluation order of fields.
The current record proposal provides no notation for accessing a single positional field:
There are a couple of implications of this.
First, I think this means that there's significant asymmetry between named and positional fields, in that you can access a named field directly on the object, but you can't do so for a positional field. So you have a constant sized syntax for reading a single named field, but the only syntax for reading a positional field requires reading all of the fields (out into a pattern match) which is fairly verbose (even if you just use
_
for all of the fields you don't care about).Second, there's a semantic asymmetry in that you can presumably access named fields dynamically (and hence write code that is polymorphic over records with named fields) but not positional fields:
Should we provide a way to read a single positional field? e.g.
record.0
etc?cc @munificent @lrhn @eernstg @stereotype441 @natebosch @jakemac53
The text was updated successfully, but these errors were encountered: