-
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
require records to have two or more fields? #2125
Comments
The general Record type is useful though and I do expect general purpose things to be built on this. You can for instance get the named and positional "fields" for a record using the public API. Not allowing records to have only one (or possibly even zero?) "fields" means these multi-purpose functions can't work with these simpler data structures. |
Can you elaborate on a use case where it would be useful to support general Records with literals with only one field? |
I could see records with a single field useful as inputs of an API. especially generic constraints For example: T foo<T extends ({ String name })>(T value) {
return ({...value, name: 'John'});
} |
It'd also be helpful for package authors who want to avoid breaking changes when they know that a future release will add new fields to the record. |
I don't think a record with a single named field is the problem (it doesn't have a syntax conflict with grouping parentheses because of the name). For One problem with identifying one-tuples with object references is that It also generalize to the question of whether |
Consider for instance a pattern for data classes where the abstract class Copyable {
void copyWith(Record data);
} You should be able to make classes with only a single field Maybe this isn't actually a good API (you lose static safety and autocomplete for the fields of the record), but I could see the appeal as well, and there may be other similar use cases that don't have the same downsides. |
Ah, I'm glad you raised this because it's an important design point. I think of it as deciding whether a tuple represents a concatenation of its elements or a collection of them. If a tuple is just its elements, then it follows that a one-element tuple simply is the element itself. There's no need to have one-element tuple expression syntax because Likewise, at the type level, if the language treats a tuple type as a concatenation of types then the type Tuples are concatenations in Swift, C#, Standard ML, OCaml, Haskell, and most other ML-derived languages. Tuples are collection-like in C++, Python, and TypeScript. I would put Kotlin in here too, though it's a little fuzzier. (Python is where I got the idea to use a trailing comma to distinguish a single-element tuple from a grouping expression.) You'll note that languages with option types tend to not have single-element tuples and languages with nullable types tend to have them. (I don't think that's a coincidence.) The thought process that led me to this for Dart was basically:
That means you can create records like: var record1 = (1, 2, x: 3, y: 4, z: 5); Here's a single record with a couple of positional fields and a few named ones. You can destructure it like this: var (a, b, x: x, y: y, z: z) = record1;
print('$a $b $x $y $z'); // "1 2 3 4 5".
I want to allow: var (a, b, y: y) = record1;
print('$a $b $y'); // "1 2 4". In fact, you don't have to destructure any named fields if you don't need them: var (a, b) = record1;
print('$a $b'); // "1 2".
var record2 = (1, x: 3, y: 4, z: 5); This should work: var (a, y: y) = record2;
print('$a $y'); // "1 4".
var (a) = record2;
print('$a'); Here we've got a record with one positional field and some named fields. We don't care about any of the named fields, just the positional one. What does this print? If we don't support the notion of one-element tuples, then the If it does that, how can you get the "1" out at all? You're basically stuck. This led me to conclude that tuples are containers for their fields and that extracting even a single positional field is a meaningful operation. Here is some related Kotlin code to show that it deals with a similar problem: val a = Pair(1, 2)
val (b) = Pair(1, 2)
println("a = " + a + ", b = " + b) // "a = (1, 2), b = 1". Objects and functions, living togetherTaking a step back, I generally think of Dart as an object-oriented language. It supports programming in a function style, but the goal is to integrate that harmoniously into the existing object paradigm. I don't want it to feel like two separate languages taped poorly together. To me, that means modeling functional styled code in terms of the existing object representation. It's why algebraic datatype-style pattern matching in the proposal is based on subtyping (as it is in Scala). I want pattern matching to work not just with a special blessed set of "functional style values" but with any kind of Dart object where it makes sense. That's why the proposal has record patterrns but models named field destructuring in terms of calling getters on objects of any type. It lets you take all of the existing object-oriented classes in the Dart ecosystem with all of their getters and immediately use them in destructuring patterns. The day this feature ships, users will be able to write: var map = {'a': 1, 'b': 2};
for ((key: k, value: v) in someMap.entries) {
print('$k: $v');
}
var (minutes: m, seconds: s) = DateTime.now(); For record patterns with named fields, then, I think the natural model is that the thing you're matching on contains the values you destructure. Since this proposal also unifies positional and named fields, it extends that model to positional fields. Positional field patterns are also just calls to getters with implicit names ( That in turn implies that tuples are collections and that there's nothing wrong or problematic with a one-positional-element tuple. It's just a class that only has The only wrinkle is coming up with an expression syntax for create a record with only a single positional element since that collides with using parentheses for grouping. A trailing comma isn't exactly beautiful, but it works. In practice, I think it will be rarely used. It's mostly about being able to support destructuring values that contain one positional element and other stuff. |
I think a lot of that makes a lot of sense, but I'm not sure I agree about #4. Why can't My problem here is that foo(
a
);
foo(
a,
);
foo((
a
));
foo((
a,
)); ...mean the same thing... except the last one. Also that I have to explain why the compiler has Opinions about these four cases that aren't obvious (or symmetric): var (a) = (a);
var (a,) = (a,);
var (a) = (a,);
var (a,) = (a); |
I'd consider not allowing partial record matches without extra syntax. That would mean writing something like var (a, ...) = biggerRecord; to match part of a bigger record. By requiring you to be explicit about there being more, we help you noticing if a tuple type changes, or you forgot about something, even though you intended to be exhaustive. The pattern match should only be allowed when the structure of the RHS pattern is statically known, so it's clear from the context which parts are not matched. I'd even allow capturing the rest with a "rest pattern" var (a, x: x, ...p) = (1, 2, 3, x: 4, y: 5, z: 6);
print(p); // (2, 3, y: 5, z: 6) so I admit I lean heavily towards records as concatenations. It's the typing of (I'd also consider a list matcher like |
Oh that's a really good point, yes. |
Good point! We could do that and say that, yes, one-element positional destructuring patterns exist, which implies that one-positional-element record values exist too, but the latter simply don't have a literal syntax. You're right that using a trailing comma for tuple literals is kind of confusing in a language that already allows trailing commas in argument lists. Sort of like how Dart didn't have set literals for many years even though it had set objects. We'd still want at least some API to create them, though. One place this comes up is user-defined extractors. This is something I do want to support. If we have that, then most extractors will return a record representing the extracted fields. I expect it to be common—perhaps the most common—that the extractor only destructures a single value. (In other words, it behaves like a conversion pattern.) For example, imagine something like: (int)? parseInt(String s) =>
// Try to parse [s] to int, return value in record on success or `null` on failure.
(bool)? parseBool(String s) =>
// Try to parse [s] to bool, return value in record on success or `null` on failure.
(bool)? parseBool(String s) =>
// Try to parse [s] to bool, return value in record on success or `null` on failure.
describe(String s) {
switch (s) {
case parseInt(n) => print('integer $n');
case parseBool(b) => print('boolean $b');
case _ => print('other $s');
}
} The return type of So in the body of these extractor functions, they'll need a way to create a positional tuple with a single field. That could be as simple as a constructor on Record or something. I do think having a literal syntax for single-field tuples would be nice, but it's not essential. |
For positional fields, yes. The proposal currently requires you to match them all. You can't silently discard them, just like you can't silently pass unused positional arguments to a function. There is also a TODO in the proposal to support a But for named fields, I don't want you to have to match them all. Since positional fields are just getter calls on arbitrary objects, "all" could be a potentially large and unwieldy set. We definitely don't want users to have to match
+1. In particular, if the thing you're destructuring changes its API by inserting a new positional field in the middle, we wouldn't want to have existing patterns continue to compile but now silently change which fields they are destructuring.
I'm interested in this too, though what the "rest" means with named fields where the RHS isn't literally a record type could get weird. I'd want to see use cases before we dig into this.
I'm not super attached to num magnitude(Record r) => // <-- "Record" here.
switch (r) {
case (x) => x;
case (x, y) => sqrt(x * x + y * y);
case (x, y, z) => sqrt(x * x + y * y + z * z);
case (x, y, z, w) => sqrt(x * x + y * y + z * z + w * w);
}; Code like this isn't great, but use cases like it come up enough that I think it can be helpful. Also, it's a potentially useful target for extension methods.
+1. The proposal states that now and has a TODO for |
I don't like this idea, at least not in the given example Here this isn't a "pattern match", but a variable declaration. Writing:
should be nothing but syntax sugar for:
avoiding the repetition of "a". There's no exhaustiveness involved here, since there's no matching done. I'd personally expect it to work like in Javascript, so that we'd be able to do:
|
Maybe it's worth explaining why an exhaustive destructuring would be needed If this was about supporting things like:
Then I'd understand But for a "var x = record", I don't see the value added. |
I like the feature in principle, but syntax-wise, I can't tell if this is defining a function or calling a function, and the idea that it might instead be declaring a variable and implicitly calling a function and the things that looks like parameters are in fact return values of a sort is not something that fills me with happiness. |
If the syntax required you to write describe(String s) {
switch (s) {
case parseInt(var n) => print('integer $n');
case parseBool(var b) => print('boolean $b');
case _ => print('other $s');
}
} then the syntactic symmetry would be broken too. (Or, in this case, there wouldn't be a |
That sounds like we are treating "normal" objects and tuples the same way. I probably wouldn't do that. Named record elements are not getters. I wouldn't expect Accessing members of an object is always optional. The object is defined in terms of its identity, its state, and its behavior. A record/tuple is only defined in terms of its contents. It's a product type. The only thing you can do is destructure (in whichever way) to project values out of the product type. We don't have subtyping between (I'm sure we can find use-cases for allowing it though. The question is how dangerous those use-cases are, and whether people would be happier with having a physical reminder, |
Oops, yes, that is in fact the correct syntax here. But note that if we supported user-defined irrefutable extractors (which I would like), then for variable declarations, it would look like: var (parseInt(x), parseInt(y)) = ("123", "345");
print(x + y); // "468". I agree that if you aren't used to the notion of patterns, it can be confusing when what looks like an expression is sort of the inverse. But that property is intrinsic to the concept of pattern matching in all languages: var a = 1;
var b = 2;
var c = 3;
var [d, e, f] = [a, b, c];
var (g, h, i) = (a, b, c);
var {5: j, 6: k} = {5: a, 6: b};
var Point(x, y) = Point(a, b); Patterns always mirror the expression syntax for the kind of thing they destructure. It's weird at first but once that clicks then it becomes an intuitive way to understand how the destructuring behaves. "Ah, this pattern looks like a list literal, I bet it accesses elements like you would from a list. This pattern looks like a map literal, I bet it accesses elements like you would from a map. This looks like a constructor, I bet it pulls out the fields that the constructor initializes." |
I admit I have a very hard time reading var (parseInt(x), parseInt(y)) = ("123", "345");
print(x + y); // "468". and it's not because I am completely unused to patterns. Would actual mirroring be var ("$x", "$y") = ("123", "345");
print(x + y); // "468". It won't work, obviosuly, because type-agnostic conversion to a string is not a reversible operation. Even as var (parseInt(var x), parseInt(var y)) = ("123", "345");
print(x + y); // "468". I find it hard to understand the data flow (but it is slightly better than the non- Maybe (var x, var y) = ("123", "345").map(int.parse); // Applies int.parse to every element of tuple. or just use binding property matchers ({parseInt(): var x}, {parseInt(): var y}) = ("123", "345"); where Or allow arbitrary expressions containing ({int.parse(it): var x}, {int.parse(it): var y}) = ("123", "345"); |
I'm confused as to what the relationship between the string literals, the parsed integers, and the variables are here, and I think that may be leading to the ambiguities. From the print statement, I see the above as: final xString = "123";
final yString = "345";
final x = int.parse(xString);
final y = int.parse(yString); So why in the syntax above are the literals themselves on the right-hand side of the equals sign? Why are the variables the ones in the parenthesis for the functions if the literals are the actual arguments? Pairing final xString = "123", yString = "345";
final x = int.parse(xString), y = int.parse(yString); final x = int.parse("123"), y = int.parse("345");
// or, more realistically
final x = int.parse(getX()), y = int.parse(getY()); In this case, @lrhn's list pattern syntax is more intuitive IMO:
Here it's obvious what's happening and where the data is going, and only step 3 is "new" to pattern matching:
|
@Levi-Lesches If that's your takeaway, then my syntax has failed. There is no Records/tuples and iterables are significantly different, because elements of records do not need to have the same type. |
Agreed, it looks weird. It may be that I chose a particularly unfortunate example since |
The proposal has changed somewhat since most of this discussion happened. In particular:
Even so, the proposal does still support records with no fields and records with a single positional field. There is a longer-term goal to support spreading records into argument lists (#1293). (Or more generally, to be able to use a record to represent a reified argument list.) To support that with as much generality as possible, that means supporting records of all shapes, since parameters lists may accept zero or only one positional parameter. There is the separate question of what syntax you use to create a record with zero fields or just one positional field. The proposal currently says:
For record types, I think the language team is basically OK with all this, so I'm going to close this out. We can definitely re-open and keep discussing it, though, if there are concerns about whether it's worth supporting them at all and/or what the syntax should be, if any. |
Semantically-meaningful trailing commas seem dangerous, given how we've been teaching people for a few years that they can transparently add or remove trailing commas for stylistic reasons. |
Yeah, it's not idea. But, for what it's worth, Python allows semantically-ignored trailing commas in list literals and argument lists and also uses |
My dear hope is that we create a language substantially better than Python. Otherwise, I'd just use Python. :-) |
I think we can be better than Python overall (for many use cases) without needing to be superior specifically in the area of "using trailing commas to indicate single-element tuples". :) |
Sure, I'm just saying that "they do it" isn't a relevant argument one way or the other. We seem to be agreed that semantically-meaningful trailing commas are dangerous and not ideal. I understand from the comment above that there's good reasons for supporting 0-field and 1-field records. ASCII gives us four sets of brackets, all of which are overloaded in dart, and dart also has one other matching pair of symbols that I can think of: We could take a page out of the pre-ASCII days and introduce another kind of syntax, like Anecdotally, Python's syntax is confusing to developers (there's a lot of questions about this on the web). I don't have a good solution here. I just think it's worrying that this: var x = (
2,
); ...means something radically different than: var x = (2); ...while these two, which look very similar, do mean the same thing as each other: var x = y(
2,
);
var x = y(2); ...especially after years of telling people to add commas just like that to control the formatter (which should definitely not be affecting semantics). |
We discussed this in the meeting this morning. There was fairly broad consensus that the current specification which uses a field on Three alternatives were considered. The first was to treat positional record fields as syntactic sugar for named fields. So The second was to remove the singleton record syntax (with the trailing comma) in favor of a constructor on The third was to remove The concern from @Hixie above is a real one. It is unfortunate that the comma in the singleton case radically changes the semantic meaning. An argument that this is not likely to be too painful in practice might be the following reasoning:
A concern with the above is that if we ever add juxtaposition as an operator (e.g. perhaps with some kind of block syntax/trail argument), we might end up in a really bad place here. The reasoning would be that currently cc @munificent @eernstg @lrhn @jakemac53 @natebosch @mit-mit @stereotype441 @kallentu |
Re how common this is, I was surprised at how many posts I found where people were asking about this for Python. (I didn't find anywhere near as many for other languages, but then Python is both more popular in general and more popular with less experienced programmers, so I don't know what to read into this.) Re the trailing comma danger, my concern is more about people removing it from tuples (and breaking their code in pretty subtle ways?) than people adding it to expressions. I guess we'd also have to decide how Is there a world where we somehow coerce scalars into one-tuples? I'm not up to date on exactly how tuples will be implemented so maybe this doesn't make much sense or would lead to too many issues. FWIW given the lack of any good options here I understand if we decide we have to go with this anyway. |
One important difference here is that Python is dynamically typed, which means that whenever you make this mistake, you never find out about it until runtime (and even then, it may "just work" for a while). In a statically typed language, most of the time you're going to immediately get a static error (not always, of course, but usually).
Yes, I can definitely see this happening. I start with an expression that looks like:
And then I decide I don't need
Or initially factor to
followed by a comma deletion. Again, I think in most scenarios you will just get a static error, but not always.
This isn't totally unthinkable, but I think it has its own warts. e.g. All of the record types are subtypes of
👍 It's definitely good to talk through the options (and lack thereof) here, but yeah, there may just be tradeoffs we have to make here. |
Ooh, your comment about static analysis made me think... one option to hugely mitigate this problem is for us to make the error messages / analyzer messages explicitly call this case out. Instead of "A value of type 'int' can't be assigned to a variable of type 'MyFancySingleTuple'.", we could have it notice that the thing being assigned is an expression in parentheses and instead say ""A value of type 'int' can't be assigned to a variable of type 'MyFancySingleTuple'; consider adding a trailing comma to change the expression into a one-value tuple literal."" or something like that. |
cc @bwilkerson @srawlins @johnniwinther on the error messages |
I have extensively considered whether we can make a one-tuple and a single value be the same thing. In short: No, not in any realistic way. Mathematically, it should be the same, since a singleton Carthesian set product is the same as the original set, X1 = X. (And the empty tuple should be the same as the It just won't fly, and not for lack of trying. We want a lot of things for records, and some of those fly in the face of treating records as mathematical Carthesian products. Then we can't treat records as mathematical set products. If we did, then If we want any kind of performance, we need to be able to predict the structure of records at compile time. Which means that we need to assume that So records nest. We also want to, eventually, be able to use records to represent argument lists. Say, for (About making |
For what it's worth, the ambiguity discovered in #2469 means that we're going to have to change the record type syntax too. If we change that to
Ooh, that's a good point. It would do the latter, and keep the var rec = (123,); I would not allow any split after the The nice thing about this is that having distinct formatting here means that properly formatted code would help users distinguish single-element records from argument lists with a trailing comma, since the only time they will ever see a |
Unless I'm missing something, that would also allow for |
The idea being discussed there is that we'd use |
We've decided that records can have zero or one positional field, and settled on a syntax as of #2535. |
https://github.com/dart-lang/language/blob/master/working/0546-patterns/records-feature-specification.md
We could also fix this by requiring that tuples have more than one field. This seems like a simplification with no cost since a record with one field is equivalent in every way other than syntax to just having that field as a variable rather than a record.
The text was updated successfully, but these errors were encountered: