-
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
Partial Record destructuring #3964
Comments
It's not completely unreasonable for a pattern with a record type as matched value type, to be able to ignore some of the fields. The hurdle is that it's not allowed for a refutable match against a non-record type, because a record pattern must ensure that it is matched against a single known record shape. |
You can also use the defined type alias to object destructure the record. However, I do support this feature. void main() {
final Big big = (field1: 42, field2: 42, field8: 42);
final Big(:field1, :field2) = big;
} |
A straightforward solution would be to enable the syntax like final (:field1, :field2, ...rest) = big;
final [x, y, ...rest] = [1, 2, 3, 4]; If the "rest" is of no interest, then final (:field1, :field2, ..._) = big;
final [x, y, ..._] = [1, 2, 3, 4]; |
Yeah, I agree this would be useful. As @tatumizer suggests, I do think it should be something you explicitly opt into, probably using the same However, as @lrhn notes, it's challenging to incorporate this into refutable patterns. If we support: var stuff = (a: 1, b: 2, c: 3);
var (:a, :c, ...) = stuff; Then users would probably expect this to work: var stuff = (a: 1, b: 2, c: 3);
if (stuff case (a: 1, :var c, ...)) {
print('a is one and c is $c');
} But that might in turn lead them to expect this to work: Object stuff = (a: 1, b: 2, c: 3); // <- Note: Has type Object now.
if (stuff case (a: 1, :var c, ...)) {
print('a is one and c is $c');
} Even though this looks similar, it's a very different program. Because now, what is the type that You can't just say "a record with fields We could say that you can use incomplete record patterns (i.e. ones with a In short, it's an interesting idea and would be practically useful. I just worry about the risk of user confusion. |
I am interested if there would be a possibility to improve inference around this type alias object destructuring: class Student {
final String name;
final int age;
Student(this.name, this.age);
}
class StudentT<T> {
final String name;
final int age;
final T something;
StudentT(this.name, this.age, this.something);
}
StudentT<T> getStudent<T>(Student student, T Function(Student) selector) =>
StudentT<T>(student.name, student.age, selector(student));
void main() {
final StudentT(:age, :name, :something) = getStudent(Student('Ethiel', 12), (s) => s.age > 5);
print('Student $name is $age $something years old');
} In this example void main() {
final a = getStudent(Student('Ethiel', 12), (s) => s.age > 5);
print('Student $name is $age $something years old');
}
void main() {
final StudentT<bool>(:age, :name, :something) = getStudent(Student('Ethiel', 12), (s) => s.age > 5);
print('Student $name is $age $something years old');
} In this example void main() {
final StudentT(:age, :name, :something) = getStudent<bool>(Student('Ethiel', 12), (s) => s.age > 5);
print('Student $name is $age $something years old');
} This is where I get a slightly confused, Swap to records and everything works as expected with no explicit types assigned but partial destructuring goes away: typedef Student = ({
String name,
int age,
});
typedef StudentT<T> = ({
String name,
int age,
T something,
});
StudentT<T> getStudent<T>(Student student, T Function(Student) selector) =>
(name: student.name, age: student.age, something: selector(student));
void main() {
final (:age, :name, :something) = getStudent((name: 'Ethiel', age: 12), (s) => s.age > 5);
print('Student $name is $age $something years old');
} |
@lrhn @munificent IMO it feels like we're trying to mix "pattern matching" and "destructuring" here. Thinking about it: Although this issue mentioned Records, the solution probably doesn't have to be unique to them. Instead of the existing final :{a, b} = (a: 42, b: 21);
final :{name, age}= Person('john', 18);
final :{length, first} = [1,2,3];
final :{entries} = {'key': value}; And this could apply to other variable definition, such as parameters: Event(onChange: (:{value}) => print(value));
// Equivalent to:
Event(onChange: (obj) => print(obj.value));
void fn(Person :{name}) => print(name);
// Equivalent to:
void fn(Person person) => print(person.name); In that scenario, we don't care about "refutable vs irrefutable pattern" as this doesn't involve patterns at all. |
This does look like an Object pattern destructuring with a different syntax. If we had a shorthand for not writing the type, strawman final :{length, first} = [1,2,3]; would just be final .(:length, :first) = [1,2,3]; Using the existing syntax. We're not mixing destructuring and matching. Patterns are designed as a syntax for both, and declaration patterns are only for destructuring. The reason to ask for var .($1: first, $2: second) = quintuple; To get the first two positional elements of a larger record, or live with the |
Yet the problems listed were about Both concerns, to me, feels like they are raised only because we're reusing pattern matching instead of having destructuring be its own thing. |
Bouncing back up to my example, #3964 (comment) This works properly and infers the generic: void main() {
final a = getStudent(Student('Ethiel', 12), (s) => s.age > 5);
final StudentT(:age, :something) = a;
} But combining the two lines into one does not infer the generic. Changing this to an iife then works on 1 line as well: void main() {
final StudentT(:age, :something) = () {
return getStudent(Student('Ethiel', 12), (s) => s.age > 5);
}();
print('Student is $age $something years old');
} That seems funky... |
What if we only partial record deconstruction on explicitly Record typed objects? This would encourage them to create a type alias, or use the entire field. |
@munificent wrote:
I do think that's a nice response to the initial post in this issue. It would work, based on the static type of the initializing expression in irrefutable contexts (declarations), and based on the matched value type in refutable contexts (switches etc.). In the case where it doesn't work, the developer would get a compile-time error (in particular, it wouldn't silently have a different semantics). Not bad! Whether it's a feature that pays for its implementation effort is another question, but I definitely think this is a viable proposal, and also reasonably readable. |
Problem:
Currently, it is not possible to use destructuring to only extract one or two variables out of a Record.
Consider a large Record:
If we want to extract
field1
+field2
but nothing else, we currently have to write:That's bloody inconvenient. It's both really tedious to type, difficult to read (due to a lot of noise), and difficult to maintain.
In particular, adding a new field in that Record requires going through every places where it was destructured.
Proposal:
When destructuring is used inside variable declarations, we could make specifying any field optional.
Using the previous
Big
record, we could therefore write:This is how Record types work in Typescript.
The text was updated successfully, but these errors were encountered: