-
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
"includes" instead of "implements" for view composition #2547
Comments
I don't like
The benefit of view class Foo(int x) {
int get foo => 42;
}
view class Bar(int x) implements Foo {
int get bar => 37;
} behaves equivalently to: class Foo(int x) {
nonvirtual int get foo => 42;
}
class Bar(int x) implements Foo {
nonvirtual int get bar => 37;
} wrt. resolution of non-virtual methods. A view is just a class-like thing where methods are non-virtual by default. What I don't like is that What I'd like about view class Foo(int _x) {
int get foo => 42;
}
view class Bar(int _x) extends Foo {
int get bar => 37;
} suggests that the The corresponding classes-with-nonvirtual-members would be: class Foo(int _x) {
nonvirtual int get foo => 42;
}
class Bar(super. _x) extends Foo {
nonvirtual int get bar => 37;
} It includes all super-class/view member implementations. For views, that's only the non-virtual ones, but it's still correct. The issue is that you can extend more than one view. Heck, we can allow all of the modifiers both: view class Foo extends Bar, Baz with Qux implements Twix {} Then you can use whatever you like, it's all the same for non-virtual methods. |
Perhaps another way to think about is that the verb here tells you something about what kinds of declarations are allowed there. In Dart today:
So the word tells you what you can do next. From that angle, |
This is going to be a pretty hard sell. The word "include" is probably one of the most ingrained terms in programming: 50 years or so of the C pre-processor has pretty well ingrained the notion that if you "include" something you are basically copying it in. It's really hard for me to believe that using "include" here is going to give users the right intuition. In general, I'm really uncomfortable with using class syntax, but with non-standard affordances. We could talk about using
Well, there are lots of types that don't have an interface (e.g. function types), and we're about to release more (record types). I don't really see the problem here, but if it bothers you it's completely straightforward to allow
This may be necessary, but it is not sufficient. If you extend something and you are concrete, it's not enough for it to be a class: it must be a class with a generative constructor. Similarly for other things: there are requirements on the things that you I think the main point of concern that I share here is that it is surprising to see members in a view class V1(int i) {}
class extension on V1 {
void v1() {}
}
view class V2(int i) {}
class extension on V2 {
void v2() {}
}
view class V3 implements V1, V2 {} I don't object to this entirely, and it is in many ways more general: you could perfectly well use these sticky extension on things other than |
The way I read
I have pretty fixed expectations of the types after A class extends a class, a mixin extends a mixin, and a view would then extend a view (or more than one, because multiple inheritance is not a problem). I guess it could be argued that The one thing that makes me prefer |
Here is an attempt to gather the main points which have been made in this thread. I added one more option: Use Subtyping
"Inherited" member implementation availability
Member declaration conflict resolution
Correct overridingDevelopers may very well expect override constraints to be enforced when a view method is declared in a superview and is also redeclared in a subview. We could enforce this, but we do not have to do it. The current proposal allows an arbitrary relationship (including: a view can declare a getter
Run-time polymorphismThe subtyping relationship among views allows for run-time polymorphism in a way that appears to be very similar to the run-time polymorphism which is supported for objects that are accessed using class types as their static type: view class V0(num n) {...}
view class V1(int i) {...}
void main() {
V1 v1 = V1(42);
V0 v0 = v1; // OK.
v1 = v0 as V1; // `v0 as int` at run time.
} The invocation of members is resolved statically, but that doesn't change the fact that we can have a reference of type
|
Good summary. (In case it was not clear, my vote is for |
This is a good point. |
I'm not in love with Have we considered |
The perspective that I keep coming back to is the perspective of the reader of the code. If I go to definition on a view class, and I see: view class V1(int i) implements V2 {
int foo() => ....
} then if I just make the obvious assumptions, I will draw the following working conclusions:
Those are all, to a first approximation, correct. It's true that The same is true for Whereas if I go to definition and see So from the reader's perspective, it seems to me to be universally better to use The objection (which is fair) is that from the writer's perspective, |
If I see a class that says If I knew that V2 declared a |
Yes, this is probably a good argument for using
Here is a class declaration that our users work with every day. That class definition claims (indirectly) to implement Are you confused by it? Do you think our users are confused? I mean, it is a bit confusing, if you dive into it. But mostly, you don't care. Most of the time, you're just going to say "oh, this class implements Now it's definitely true that the interface/implementation split is pretty standard now, and people have internalized it in a way that they won't have for something new like a view class. So I certainly don't claim that this is a non-issue. But I do question whether this is something that's actually going to be a pain point for users in practice? |
One area where pre-existing mental models may be helpful for developers who are getting acquainted with views is conflict resolution:
So we might want to link these properties to the handling of conflicts with views: If every conflict must be resolved by redeclaring the conflicted name then we might want to use |
One advantage of view class FooView implements BarView {
final int x;
FooView(this.x) : super.special(x);
} to explicitly invoke the superclass constructor, if I really, really want to? Or should I just write it as: view class FooView._(BarView x) {
FooView(int x) : this._(BarView.special(x));
} (Can I have Apart from that, after talking more with both @eernstg and @leafpetersen, I'd probably be satisfied with It is as if view classes can be used both as view superclasses, and also as view mixins, like some classes can. Most views will only extend one other view, in order to inherit its API, and using Views which combine more than one super-view will likely have some of the other views be "mixin-like" (intended to go on top of another view and add or modify a few members), and it might make sense to the author to use You get to show a kind of intent in whether you use (But not calling superclass constructors makes me worry a little about |
I think
It's confusing for users who don't understand In other words, with Whereas with a view class, if we use |
Closing; it's |
I'm going to take one last shot of making us reconsider the syntax. After having thought about it more, I worry that using TL;DR: We should use Because that's how I'd write a similar non-inline wrapper class: Extend another wrapper to get its implementation, and implement a type supported by the wrapped object by forwarding. It's more consistent with how we write classes, and we've chosen to make inline classes look like classes, so I think we should be consistent with that. Inline class members are not extension members, which apply to all subtypes. The current I've gotten used to the The reason I try to push for this now is that it opens up using We have precedence for doing that: Inline classes have We allow those invocations because the representation object is guaranteed to have the methods, but we don’t go the extra step of saying that I want to allow
And I want to free up We can allow using
Just like a normal wrapper class. We don't need to introduce the |
One of the features I LOVE in @projectlombok is the class A extends B {}
// When B is a final class and can't be extended
class A implements B {
@delegate
final _b = B();
}
// Then generates
class A implements B {
final _b = B();
C c() => _b.c();
D d() => _b.d();
E e() => _b.e();
} |
@Wdestroier wrote:
I think this is covered pretty well by #2506. |
Awesome, @eernstg! I didn't see it before. |
I tend to prefer using The really big difference comes up when we consider constructors. I don't think it should be implied that an inline class must call every constructor of the inline classes which are specified to be its supertypes. With I would be quite happy about having the ability to request a subtype relationship from an inline class to any given list of supertypes of the representation type, e.g.: inline class C implements C1, C2, int {
final int i;
C(this.i);
... // Other members.
} I would prefer to keep the addition of members to the interface of The point is that this allows us to create an inline class inline class C implements C1, C2, num {
final int i;
C(this.i);
export i show int hide isEven;
... // Other members.
} This means that we're obtaining forwarders for every member of the interface of You could of course offer these features as a package and take away some of the flexibility (e.g., you could say that |
We're going with a "like a class" approach, and with that mindset, I'm not too worried about subtypes that don't inherit supertype members. I also want to allow you to forward members to the representation class without having to implement its interface, probably by declaring abstract members (which adds members to your interface without adding implementation, just like an In practice, I’d expect inline classes to fall into one of three categories:
The case which is not supported is being assignable to (a superclass of) the representation type, but not inheriting its members. That’s probably also going to be confusing even if it is technically possible. If it was a completely stand-alone feature, I might think differently. In any case, moving away from using (Also, all these complications, and |
We could introduce a default (We would need to handle conflicts, of course, because This would also be a non-breaking enhancement of the treatment of
I think those categories illustrate quite nicely that we might want to express forwarding and subtyping separately: 'Reinterpretation' calls for a near-complete removal of the existing interface members (everybody has so far agreed that
This is the change that I do not consider to be an improvement. I think there's a lot of truth in the explanation that inline members are similar to extension members (no polymorphism / late binding, no dynamic invocations, applicable to every receiver whose static type is a subtype), and I think the handling of object construction is very different. Also, if you say |
I'm now going to give an argument that what we are doing now is perfectly good and reasonable. The consistent and explainable story I'll use for using Inline classes are static-only skin-deep types that act as wrappers around a real object, represented by a new type. Member invocations on that type are resolved statically to the members of the inline class. Subtyping and assignability is only to Since every member is statically resolved, there is no inheritance, no overriding. Such a supertype must (currently) be another inline class type. A supertype must be compatible with the type of the representation object of the inline class. The effect of an inline type I with representation type R implementing a type T is that:
If two implements types introduce the same member name, we have a conflict, and the inline class must declare a member with that name to resolve the conflict. This is a simple explanation because it is based on the rule "a value of type (We don't event have to disallow cycles. We may want to, for practical reasons, but it's not a structural relation. Even member forwarding in both directions will work, because there won't be a member unless one of the types declares one.) This description is also compatible with (Then we can discuss extensions, like allowing So, all in all |
I'll close this issue: This proposal was not accepted. |
The views proposal uses
implements
to allow one view type V1 to build off other view types V2, etc. As I understand it, this clause does two things:All of the instance members defined by V2 are made accessible as instance members on V1. By "instance" here, I mean that the constructors and static members of V2 are not made available. The dispatch is still purely static. It's almost as if the instance declarations in V2 have been textually copied to V1.
V1 becomes a subtype of V2. This allows establishing a subtype hierarchy among a family of related view types who all share a related representation type.
This behavior seems totally fine to me. However, I find
implements
a confusing word to express that, for a couple of reasons:In classes,
implements
specifically means that you do not inherit any implementation. Of course, with views, there is no real "inheritance" going on, it's just that the subtyping induced allows the members on V2 to be available as extension members on V1. But informally, it feels very strange that an implements clause gives you implemented functionality.In classes,
implements
adds a requirement to the class. When you add animplements
, your class now has an obligation to define or inherit implementations of all of those members. Addingimplements
can require you to do work in your class declaration. With views,implements
gives you stuff "for free" and doesn't require you to add anything (except possibly redeclaring to avoid collisions).In classes, and in other languages,
implements
implies some level of runtime polymorphism and dynamic or virtual dispatch. It suggests that the thing being implemented is an interface and the class can now be used wherever that interface is allowed, and its instance members are now polymorphic implementations of that protocol. None of that applies to views where all dispatch is entirely static.In short, it feels like a confusing keyword choice to me. Instead, I propose
includes
:I think that word conveys that all of the functionality defined by the superviews is now available by the subview.
It might be confusing that a new word
includes
also induces a subtype relation. But I believe we already have that in Dart and it doesn't seem to cause much confusion in practice. The keywordsextends
,implements
, andwith
all also mean that the class containing that clause becomes a subtype of the type in the clause. So this would just be one more keyword where that's true.I also think the subtyping aspect is less important than the code reuse aspect. When users are reading a program, they mostly want to know where the code that gets called is and how it is reached. Using
includes
suggests that "Ah, this method I can see being called on V3 must have been included from V1".The text was updated successfully, but these errors were encountered: