-
Notifications
You must be signed in to change notification settings - Fork 214
[Class Modifiers] Why do we propagate base
through implements edges.
#2909
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
The current transitive property of A class outside of the library cannot implement, and cannot allow others to implement, that interface without providing implementation for the private members, and since they cannot provide that implementation, they're not allowed to implement. There is nothing preventing us from removing the I'd be fine with allowing you to reopen/ignore We'd then have to decide whether a subclass in another library prevents implementation or not. It's not going to be as simple as "has superinterface marked The most likely specification would be something like:
(Aka, if an interface has a I think this works, but I'll have to go over it a few times to make sure. (The current rules are easy in comparison: Superinterface marked |
A lot of the modifiers spec tries to work around this issue -- restricting // a.dart
class A { int _a = 1; }
void test(A a) => print(a._a); // b.dart
import "a.dart";
class B extends A { }
class C implements A { }
void main() {
test(A()); // OK
test(B()); // OK
test(C()); // Compiles, but crashes at runtime
} If this wouldn't crash -- somehow, |
The failure ('Compiles, but crashes at runtime') is one of the major reasons why the upcoming |
I agree that
and why #2918 would probably be better served to address that issue at its root. |
Yes, there is a larger motivation that a library author should be able to author a class and guarantee that all subtypes of it inherit from it. And nothing another library can do can break that invariant. But, as you say, that doesn't explain why we propagate The proposal is generally OK with letting the library author break an invariant themselves in other areas. And, indeed, the proposal used to allow breaking it in this case too. If I remember right, after a lot of discussion (which is probably buried in an issue or PR somewhere) Lasse and I felt that it was the right trade-off because otherwise the transitive rule for "cannot be implemented locally" gets more complex. Consider: // lib.dart
base class A {}
class B implements A {} // Let's say we allow this.
// other.dart
import 'lib.dart';
base class C extends A {}
class D implements C {} // ERROR: "D" must have "base" since it inherits from A.
class E extends B {}
class F implements E {} // OK: "B" has already broken the chain.
// Where it gets weird:
class G implements C, E {} Is As Lasse says, we could probably come up with a specification that sorts this out. Basically saying that if there is at least one path through the supertype DAG that reaches a type that re-adds implementability before reaching a type that prohibits it, then you're good. But that felt really complex and subtle to me. My time working on games and package managers has endeared me to pathfinding algorithms, but this felt like a dubious use of them. :) Instead, I suggested that it's simpler, safer, and easier for users to understand if you simply can't re-add the implements capability at all, even in your own library. You're correct that it's more restrictive than the proposal is. And it's not necessary to be this restrictive. But I wasn't confident that users (or even we) could fully understand the more permissive algorithm where we look for a valid path in the superinterface graph. If this turns out to be too onerous of a restriction (unlikely, I think) we can always loosen it in a future release. But, especially now given the current schedule, I thought it was good to have something simpler even if it's more restrictive. |
Closing this, I think no further discussion is needed at this point. |
In the section on transitive restrictions, the class modifiers proposal says the following:
The subsequent section on "Inherited restrictions" motivates preventing re-adding capabilities to classes from other libraries, but is silent on the question of re-adding capabilities to classes in the same library.
Nonetheless, the section on disallowing implementation chooses to disallow some (but not all) capabilities to be re-added to classes in the same library. For example:
The motivation I've heard for the core restriction is that we wish to allow a user to write a class for which all subtypes are guaranteed to inherit from (and consequently call the constructor of) the original super type. But of course, this is not true - the example above is legal if I add
base
to the three subtypes, and yet there are still instances ofVehicle
that neither inherit fromVehicle
nor call its constructor.Why do we choose to propagate the
base
restriction over implements edges? This seems under motivated to me, and would seem to disallow useful patterns.cc @dart-lang/language-team
The text was updated successfully, but these errors were encountered: