-
Notifications
You must be signed in to change notification settings - Fork 213
Records: Destructure
superinterfaces could be subtype related
#1278
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
TL;DR: Yes, I agree we could do this, in exactly the manner you propose. I'm currently leaning towards not.
I wouldn't consider that width subtyping because a record type only implements the one
Yes, I considered moving records outside of the object hierarchy, but I think unboxed values (of any type) should be an orthogonal feature that we may or may not do. It felt weird to me to have records not be subtypes of Object when numbers are, after all. If we're going to have unboxed non-Object values, I think we should consider that for all of the built-in types and do it as a separate feature.
That is a goal I was hoping to enable, so it's good to hear you confirm this. :)
This is what I would consider to be width subtyping. I considered this, and it's not off the table, but I'm leaning against it. My intent is that the var (a, b, c) = obj; Then var four = (1, 2, 3, 4);
var (a, b, c) = four; I'm not deeply morally opposed to allowing destructuring to silently discard unmatched positional fields, but I'm leaning away from that. I think most of the time doing so would be an error. I also like having some amount of symmetry with positional function arguments/parameters where we consider it an error to pass more arguments than a function has positional parameters. So, one way I look at the But for built-in records, and most classes, I think users will generally want to match all the fields and have the type checker tell them if they didn't. When they want to opt out of that and deliberately only match a subset of the positional fields, I'm hoping we can have some kind of Also, I only consider this to be the case for positional fields. For named fields, I don't think users have an expectation of "completeness" when it comes to matching and for those I intend it to be more ad hoc and structural. You can match the named fields you want and ignore any others the value may expose, whether that value is a record or an instance of a user-defined class. I think that aligns with user intuition where positional parameters default to required in Dart, but named parameters default to optional. |
A class can't implement a structural type (unless we special-case it and say that you can, obviously). If the It feels slightly inconsistent to me that you can do that for positional tuples, and ignore the named entries, but you can't ignore further positional entries. I don't particularly want a four-tuple (
I am. Maybe not deeply, but I'd still prefer not to go there. It's too easy to hide a mistake if you start ignoring type-mismatches (it's exactly the same as the "overapplying function invocations" request). Now, we could start introducing a type like |
Great question. My goal is to enable compilers to efficiently compile records. That means passing their fields in registers when possible instead of heap-allocating, and inlining field access. That is a lot harder when record types are a polymorphic interface that any class can implement. This is the same reason you can't implement So there are two levels of types in the proposal. Record types are the concrete types only inhabited by actual record objects. The
cough cough |
There was a reason we stopped saying that such classes implemented the function type. Now they're just implicitly convertible to a function type. |
Kind of off-topic but: I find it quite saddening that this change was made. class MyClass {
void call() {}
}
void main() {
void Function() example = MyClass();
print(example is MyClass); // now always false, used to be true
} That allowed APIs to provides both a simple syntax using closures and an advanced syntax using delegates, like: object.addListener((value) => print(value));
object.addListener(
Listener(
onchange: (v) => print(v),
onDispose: () => print('disposed'),
),
); |
You could write that code, but you couldn't type it. The parameter type of |
@munificent wrote:
The reason why I called that width subtyping is that it would presumably cover arbitrary sets of named fields as well, cf. this section:
There was the issue about dropping some positional fields by accident, and I agree that this would be a thing to look out for:
var four = (1, 2, 3, 4);
var (a, b, c) = four;
I like the fact that this creates some connections between records and actual argument lists, which would be helpful if we will support the use of records as argument lists (say, if From this perspective it makes sense to flag situations where not all positional arguments are used. But to be consistent we should then also flag situations where not all named arguments are used — it is an error to pass a named argument which is not expected! The other perspective (which is arguably more likely to match actual usage) is that a record is used as a value, that is, as an immutable object. From that perspective it is just a matter of normal subtyping that we are able to drop some positional (and named) fields: However, we can still ensure that a pattern declaration like With this approach, a pattern can be used to destructure a concrete record type, and we won't drop positional fields. We can freely make the choice of allowing or disallowing named fields to be dropped. This means that a certain amount of flexibility is lost, but we do get the effect that it is an error to initialize a pattern declaration with a record of a different shape. We could choose an intermediate level of strictness where downcasts from I think that might work quite well. |
The subtyping is arbitrary (at least slightly). You suggest that However, since |
@lrhn wrote:
I don't think that would be particularly natural. So the arbitrariness is very slight. ;-) But the other example goes in the other direction, |
True, got the subtyping order wrong. Any tuple with exactly two positional elements implements |
Right, I'm just exploring the options. With the strict proposed model, But if we are allowed to drop the named fields using the For the pattern matching declaration, we can of course have syntax that allows dropping a suffix of the list of positional fields and any subset of the named fields (so we don't actually need the var (a, b, c) = (1, 2, 3, 4, name: 5, name2: 6); // Error.
var (a, b, c, name2: d, ...) = (1, 2, 3, 4, name: 5, name2: 6); // OK. |
Have you considered a single generic Destructure interface instead? abstract class Destructure<T extends Record> {
T destructure();
} Then an example: class Vector3 extends Destructure<(double, double, double)> {
final (double, double, double) _data;
Vector3.zero() : _data = (0, 0, 0);
(double, double, double) destructure() => _data;
}
var (x, y, z) = Vector3.zero(); |
@tatumizer I didn't see your edit, really wish GitHub would send edits through email. I don't think this feature would be associated as "records" in the minds of developers, so I don't understand what is gained by the |
I have, yes. Another way we could define the abstract class Destructure1<T0> {
T0 toRecord();
}
abstract class Destructure2<T0, T1> {
(T0, T1) toRecord();
}
abstract class Destructure3<T0, T1, T2> {
(T0, T1, T2) toRecord();
}
// ... That adds some complexity to the semantics of destructuring. With this, there is a multi-step process for destructuring an instance of a user-defined class:
In theory, compilers may be able to optimize most of that away, but I don't like relying on "sufficiently smart" compilers for performance. Also, this mechanism would only apply to positional fields. For destructuring named fields, I explicitly do want to call getters on the original instance because that lets you immediately used named field destructuring on user-defined classes without having to add any explicit destructuring support. If it's got getters, you can destructure 'em. It felt weird to me to use direct getter calls for the named fields and a separate
I admit that having to declare We can't do C#'s approach directly because Dart doesn't have output parameters, which is the primitive functionality C# uses to have a single function call emit multiple values without needing to allocate some other primitive aggregate type. (You could do something clever by having the destructuring pass a closure to the In practice, I think most classes won't expose positional destructuring. They will either use named destructuring or extractors (out of scope here, but basically a named conversion step between the original value and the destructured result). The relatively small number of classes that do directly support this—think classes like MapEntry—will clearly be data-like and oriented towards this use so seeing a couple of |
I was talking about the C# 8.0 recursive pattern object matching: if (obj is Foo{Bar: var bar, Baz: var baz) foo) {
// foo := obj as Foo
// bar := foo.Bar
// baz := foo.Baz
} There are no out-parameters here, just getter calls. With that pattern matching, you don't need to convert to an intermediate tuple in order to pattern match against an object. A class should almost never have a An example which can be run directly: using System;
public class Program
{
public static void Main()
{
Object o = new Example();
if (o is Example{Foo: var v} e) {
System.Console.WriteLine(e.Foo);
System.Console.WriteLine(v);
}
}
}
class Example {
public int Foo {
get { return 42; }
}
} A "Destructor"/"Destructurer" is not needed for objects. It's a projection function, and the appropriate projections of an object is its getters. |
Ah, interesting, I wasn't familiar with that feature.
Right, this is how I generally expect most user-defined classes to be used in pattern matching. I think named fields will be the common case and the pattern match syntax for those desugars to calling getters on the instance. In other words, I want this to just work: class Point {
final num x, y;
}
test(Point p) {
var (x: x, y: y) = p;
print("$x $y");
} But, for some classes—likely a relatively small minority—positional destructuring is obvious enough and the brevity useful enough to be worth having the class opt in. For example, I'd really like MapEntry to be positionally destructurable so you can do: test(Map map) {
for (var (key, value) in map.entries) { ... }
} |
For now at least, I have removed the |
Cf. https://github.com/dart-lang/language/blob/master/working/0546-patterns/records-feature-specification.md.
In the proposal, the type of a record with
N
positional fields (N <= 15
) is a subtype ofDestructureN<T1, ..., TN>
for some typesT1 ... TN
. This introduces a special kind of width subtyping (records do not otherwise support width subtyping). So it's worth considering the price.The use of types like
Destructure2<int, int>
will force the records to be heap allocated, but other types likeObject
could be used and have a similar effect already. However, it is probably possible to use a memory layout where access to the statically known number of fields can use fixed offsets, which means that the performance can be good except for the cost of the boxing operation itself.However, it would probably not cause any additional performance issues to enable access based on any smaller number of positional fields than the actual number. This could be supported by declaring these classes as follows:
The text was updated successfully, but these errors were encountered: