-
Notifications
You must be signed in to change notification settings - Fork 213
Should record fields start at $0
or $1
.
#2638
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
Comments
Nooooo. Zero-based everything. The name is a field index ( But I'm still somewhat tempted to just not have positional field getter expressions at all since the names are pretty ugly with the leading |
But you can't index fields, so this is the only place that this index surfaces. If I were to name them myself, I'd be very likely to do things like Same if I write parameters, Maybe we can allow you to use the static names of record fields as static aliases for the (int octet1, int octet2, int octet3, int octet4) ipv4Address = ...;
var int32 = (ipv4Address.octet1 << 24) | (ipv4Address.octet3 << 16) | (ipv4Address.octet2 << 8) | (ipv4Address.octet1); Basically, a static record type with named positional fields introduces implicit extension getters on the value. Guess I just need to make my own extension Project2<T1, T2> on (T1, T2) {
T1 get p1 => $0;
T2 get p2 => $1;
} projection getters 😉. Compare also to the Tuple library which uses (UX testing?) |
I think for consistency's sake it's a lot easier to explain that "just as the first element of a list is |
For comparison, I just did a quick prototype of a parallel-wait extension on records of futures. extension FutureRectord3<T1, T2, T3>
on Vector3<Future<T1>, Future<T2>, Future<T3>> {
Future<Vector3<T1, T2, T3>> operator ~() {
final c = Completer<Vector3<T1, T2, T3>>.sync();
final v1 = _FutureResult<T1>($0);
final v2 = _FutureResult<T2>($1);
final v3 = _FutureResult<T3>($2);
var ready = 0;
var errors = 0;
void onReady(int error) {
errors += error;
if (++ready == 3) {
if (errors == 0) {
c.complete(Vector3(v1.value as T1, v2.value as T2, v3.value as T3));
} else {
c.completeError(ParallelWaitError(
Vector3<T1?, T2?, T3?>(v1.value, v2.value, v3.value),
Vector3<AsyncError?, AsyncError?, AsyncError?>(
v1.error, v2.error, v3.error),
));
}
}
}
v1.onReady = v2.onReady = v3.onReady = onReady;
return c.future;
} where One of all these numbered things is not like the other. In every other case where I had three numbered things, I'd naturally number them as 1, 2 and 3. Just as I would in a parameter list. (I recommend trying to write something realist with records, using the |
If I saw that code using |
I named every variable consistently with its position, starting from 1, like I would have done in any other case. If I had to write a generic function taking three typed parameter, I'd write void foo<T1, T2, T3>(T1 v1, T2 v2, T3 v3) => ... every time. Starting from zero wouldn't occur to me. If the input was a list, I might do: void foo<T1, T2, T3>(List values) {
T1 v1 = values[0] as T1;
T2 v2 = values[1] as T2;
T3 v3 = values[2] as T3;
} I might use Indices are different from names. Records are not lists. They are closer to parameter lists than lists, and I'd never start a parameter list at It might all come down to perspective. I can see other languages do different things. The |
But the |
We've talked about this, but it's a dead end. Either the field names are part of the type or they aren't. You can't have it both ways or it gets weird: (int a, int b) ab = (1, 2);
(int b, int a) ba = (3, 4);
var either = flipCoin ? ab : ba;
print(either.a); // ???
I have to admit that when I number parameters or type parameters, I start at
You know, now that you mention it... We could simply not have positional field getters defined by the language at all. Then if users want some, they can define (or reuse) their own extensions like this and name/number them however they want. That would also avoid all of the problems where an implicit positional field getter collides with a named field one as in: var wat = (1, 2, $0: 3, $1: 4); And because of this, it means there are fewer edge cases when it comes to being able to spread records into parameter lists. Instead of trying to come up with a sufficiently unusual positional field getter name (hence the ugly I could even see someone defining: extension Cardinals<T1, T2, T3> on (T1, T2, T3) {
T1 get first {
var (first, _, _) = this;
return first;
}
T2 get second {
var (_, second, _) = this;
return second;
}
T1 get third {
var (_, _, third) = this;
return third;
}
} The main problems with this I can see are:
|
To be blunt, this seems like just a terrible idea to me. As you observe, we don't have anything like the kind of row polymorphism you would need to make the code re-use work well. If people actually go this route, it will be a mess. There will be inconsistently named, redundant, and highly verbose helpers scattered all over the place. And if you accidentally end up with two extensions that define If we really truly believed that no-one will use getters (I don't), we could leave them out. But leaving them out in favor of "just define extensions" just seems like a really bad idea. |
Just to expand on this a bit further, here is a small set of list pair helper methods written as extension methods using positional getters: extension ListPair<S, T> on List<(S, T)> {
List<R> mapFirst<R>(R Function(S) f) => map((p) => f(p.$0)).toList();
List<R> mapSecond<R>(R Function(T) f) => map((p) => f(p.$1)).toList();
List<R> map2<R>(R Function(S, T) f) => map((p) => f(p.$1, p.$2)).toList();
List<S> firsts() => map((p) => p.$0).toList();
List<S> seconds() => map((p) => p.$1).toList();
(List<S>, List<T>) unzip() => (firsts(), seconds())
} Here is the same code written using pattern matching: extension ListPair<S, T> on List<(S, T)> {
List<R> mapFirst<R>(R Function(S) f) => map((p) {
final (v0, _) = p;
return f(v0);
}).toList();
List<R> mapSecond<R>(R Function(T) f) => map((p) {
final (_, v1) = p;
return f(v1);
}).toList();
List<R> map2<R>(R Function(S, T) f) => map((p) {
final (v0, v1) = p;
return f(v0, v1);
}).toList();
List<S> firsts() => map((p) {
final (v0, _) = p;
return v0;
}).toList();
List<S> seconds() => map((p) {
final (_, v1) = p;
return v1;
}).toList();
(List<S>, List<T>) unzip() => (firsts(), seconds())
} I know which version I would prefer to be writing and reading. If we had parameter patterns, the code above could be written without using getters ... but we don't. But even if we had parameter patterns, you still have expression oriented code where you don't care about a field. Continuing the theme above: extension ListPair<S, T> on List<(S, T)> {
S firstOfFirst() => first.$0;
T secondOfFirst() => first.$1;
} vs extension ListPair<S, T> on List<(S, T)> {
S get firstOfFirst {
var (v, _) => first;
return v;
}
T get secondOfFirst {
var (_, v) => first;
return v;
}
} Forcing the user to bind variables in a block in order to use a value once is just noise. I don't love the positional getter syntax, and I'm open to using an alternative, but I really do think this is something that we want to have (and I think I'm by far the person on the team who has spent the most time working with tuples, so I do put a bit more weight on my opinion here than I normally would). On the original topic of this issue, I will admit that when I wrote the first method above, I initially used |
I don't have a strong opinion here, but I do have the same preference as @lrhn in this area: Numbering from zero is justified when we're considering indices as offsets ( In contrast, the first element of anything that doesn't already have a firmly zero-based convention is 'first', not 'zeroth'. |
What I really want for extensions is pattern matching on the extension FutureTuple2<T1, T2> on (Future<T1> f1, Future<T2> f2) {
Future<(T1, T2)> operator~() {
var r1 = record(f1);
var r2 = record(f2);
// ....
}
} Pattern has to be a valid declaration pattern, so we can extract a type schema from it to match against the static types at calls. We'll get that eventually. (Because we will get patterns as parameters, and we'll get primary constructor-like syntax for extensions when we get them for inline classes. I have stated my goals 😁!) I agree that not providing a canonical way to access positional elements of a record type will cause people to create their own. It's something that you really only need one of, and we can do it for all record types, which nobody else can. I just happen to prefer starting at (We discussed using |
I agree that a numerically named getter doesn't need to be treated the same as an index/offset. I don't have strong feelings about starting at |
If the syntax is changed from |
Having lists with indices starting at 0 and thinking of the elements within the list as first, second, third, etc. are not mutually exclusive cases. I, at least, don't think of a list as having a zeroth element at the start.
Switching now to 1-based indices will introduce a very glaring irregularity that doesn't provide nearly enough benefit over 0-based indices to be justifiable in implementing. Combined with the peculiar |
I poked around again to see what other languages do: Dedicated syntax
Numbered identifiers
Some sort of dependent typing
Use
|
Python and TypeScript tuples start at 0 too (they may have been purposely omitted). |
I can see why comparing Dart's records to other languages with tuples is valuable, but Dart as a whole is very close to the likes of Java, Python, and TypeScript. These languages all start from 0. Dart also uses 0-based indexing for lists, To have records start at 1 may fall in line with other implementations of tuples but would be pretty inconsistent within Dart and the assumptions that new Dart developers may have. I strongly believe intuition and simplicity outweigh "correctness" in cases like these and choosing
In other words, the benefit is that every developer comes into programming having been taught "computers start counting at 0", and will probably assume that everything is 0-indexed. |
This is where the main disagreement lies I think - I get that |
Said another way, ask yourself to compare
I would say |
It's the right question to ask, and I think There is no indexing, no computation of integers. It's just a (very short) name, one of several numbered names. So reasonable people disagree. What will we do :) |
How about |
Even if these fields aren't designed to be used like array indexes with variable access, is it possible some future language/SDK feature would have a reason to do this in some capacity? Static metaprogramming? Some kind of serialization? |
FWIW, I'm in the "start at That being said, I will lose |
Another argument for |
Repeating my comment on this other issue: I wanted to get some actual data about whether users prefer numbered lists of things in their code to be zero-based or one-based. I did some scraping. My script looks at type parameter lists and parameters. For each one, it collects all of the identifiers that have the same name with numeric suffixes. For each of those sequences, it sorts the numbers and looks at the starting one. After looking at 14,826,488 lines in 90,919 files across a large collection of Pub packages, Flutter widgets, and Flutter apps, I see:
So there's a slight preference for 1-based, but not huge. Looking at parameter lists and type parameter lists separately:
The stark difference here suggests that may be some outlier code defining a ton of type parameter lists with a certain style. Indeed, if we look at the number of sequences in each package:
So ffigen (whose names suggests contains a ton of generated code) heavily skews the data. Really, what we want to know is not what each sequence prefers, but what each user prefers. If only one user prefers starting at zero and everyone else prefers starting at one, but that user authors thousands of parameter lists, that doesn't mean they get their way. To approximate per-user preference, I treated each top level directory as a separate "author". For each one, I looked at all of the sequences in it to see if they start at one, zero, (or both):
While there are many sequences that start with zero, they are heavily concentrated in a few packages like ffigen and realm. When you consider each package as a single vote for a given style, then there is a much larger number of packages that contain one-based sequences. If you look at them, each one-based package only has a fairly small number of sequences. But there are many of these packages. That suggests that most users hand-authoring type parameter and parameter sequences prefer starting them at one. Based on that, I think we should start positional record field getters at 1 too. We discussed this in this week's language meeting and reached consensus to change the starting index to 1. I don't think it's a perfect solution, but I think it's the overall winner. |
Wow, nice job finding high quality data to answer such a subjective question. As someone who had been previously arguing for zero-based, I'm very much convinced by this data that one-based will actually be more intuitive for most people. |
Last minute bike-shedding, I know.
We have defined positional record fields to be accessible as
$0
,$1
etc. getters.While starting at zero is good for integer indices (like arguments to
operator[]
), I'm not sure it is the best choice here. The getters are not integers, you won't have to do arithmetic on them, so starting at zero is not a benefit in that regard.I worry that users will expect think of them as "1st" field, "2nd" field, etc., and therefore expect them to start at 1.
(I worry about that, because that's what I catch myself thinking.)
If it's not too late, I'd like to suggest we start at
$1
instead.(I promise I'll fix our test files if we make the change!)
The text was updated successfully, but these errors were encountered: