-
Notifications
You must be signed in to change notification settings - Fork 213
Inferred interface / Structural type system (Golang-like interface) #281
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
Subtype relationships in Dart are nominal for class types and structural for function types and for the type arguments of parameterized types. This implies that It can of course be debated whether that's a good choice (for any set of design goals that we might come up with), but nominal class subtyping is certainly such a deeply rooted property of the language Dart that we could not change the language as proposed without causing massive breakage. That said, there are also a number of reasons why the structural subtyping rules might not be an improvement. For instance, there's the old issue of misinterpreting an interface: class Plant {
void shoot() { /* ..break out of the seed and start creating a cute little plant.. */ }
}
class Cowboy {
void shoot() { /* PULL A TRIGGER! */ }
}
main() {
Plant rose = Cowboy(); // OK if using structural subtyping rules.
standInFrontOf(rose);
rose.shoot();
} If we adopt the idea that an interface can imply a certain set of expectations about the meaning of each of the members (such as Another issue is that structural typing would imply that basically no variable accesses could ever be compiled to read and write memory directly, they would always have to call a getter/setter: If we can show that the possible run-time types of the value of a given expression is a specific set of classes then we may determine that every one of them has a field So there are both conceptual and technical reasons why structural typing might be a step backwards rather than forwards. |
This would be a fundamental change to the language, so isn't feasible at this point. Sorry. |
Thank you for the detail explanation, I really appreciate your thoroughness. That being said, I don't necessarily agree with the final conclusion that it is conceptual a step backward (I'm not arguing about technical reasons as I am the language users here, not the language/compiler engineer :D). As you already pointed out, it's up to for debate, and depends on the goals of of the designed language. The reason I brought up Golang is that it's also coming from Google (even though we can arguably say that there are many internal startups within Google). I also understand the reason you brought up "old issue of misinterpreting an interface", and I believe that in Golang community, the judgement "can I trust this operation to do the right thing?" is up to the interface "user", while right now in Dart (and Java or other similar languages), is up to interface "implementer". Moreover, with background in and tendency to use functional programming, I would say that at least myself can hardly be in such situation in which I have to wonder "can I trust this operation to do the right thing?", because I'm more interesting in getting values or computing values in pure manner (i.e. no side effects). |
Actually, I think I can more or less achieve the behavior. Let's say we have abstract class ToJsonable {
String toJson();
} class Explicit implements ToJsonable {
@override
toJson() {
return "00";
}
}
class Implicit {
toJson() {
return "FF";
}
}
class NoJson {
} final Explicit e = Explicit();
if(e is ToJsonable) {
e.toJson(); // "00"
}
try {
(i as dynamic).toJson(); // "00"
} catch(e) {}
final Implicit i = Implicit();
if(i is ToJsonable) {
i.toJson(); // unreachable
}
try {
(i as dynamic).toJson(); // "FF"
} catch(e) {}
final NoJson n = NoJson();
if(n is ToJsonable) {
i.toJson(); // unreachable
}
try {
(n as dynamic).toJson(); // not executed but throw error
} catch(e) {
// error is thrown here, but we intentionally ignore
} The question is whether we should go with this approach from the syntactic and performance perspective. |
There's one difficulty with the decision to put that responsibility on the shoulders of the "user": In terms of the This means that the person who thought "a On top of that, again, the person who wrote the code with the cast might not even notice, if the type system says "No explicit cast needed here, a But, returning to the motivation, why couldn't you just do this: abstract class ToJsonable { String toJson(); }
class Explicit implements ToJsonable { toJson() => "00"; }
class Implicit { toJson() => "FF"; }
class NoJson {}
// Bridge the gap with a wrapper class.
class ImplicitJsonable implements ToJsonable {
final Implicit implicit;
ImplicitJsonable(this.implicit);
String toJson() => implicit.toJson();
}
main() {
final e = Explicit();
e.toJson(); // "00".
final i = Implicit();
// We know that we want a `ToJsonable`, and at this point we know how to get it.
final ij = ImplicitJsonable(i);
ij.toJson(); // "FF".
final NoJson n = NoJson();
// Can't help `n`, unless we write a wrapper class for `NoJson`.
// We can still try it dynamically, of course, in case we know
// that the actual value of `n` _does_ have a `toJson()`.
try {
(n as dynamic).toJson();
} catch(e) {
// error is thrown here, but we intentionally ignore
}
} |
No one said it's a step backwards, just that what you're asking for is a different language. It's as if we'd opened a burger restaurant and now you are asking us to turn it into a dessert bakery. There's nothing wrong with cake and pie, but... we sell burgers. Our kitchen is only equipped to cook burgers. Everyone already visiting our restaurant is coming here for the burgers. All of our expertise is in cooking burgers. We can't just stop being a burger joint. |
Well I was respectfully replying to eernstg's conclusion
About your analogy
I think it's somewhat true, and as in the previous reply
I'm just the language users here, and have been invited by my lover, Flutter, to this restaurant. Even though it's a burger restaurant, there's no harm in asking whether you serve vegan food or not 😆 |
@eernstg for the Anyway, going back to my example, the reason I would like to having it, is because File A abstract class ToJsonable {
String toJson();
} File B abstract class ToJsonable {
String toJson();
} My class implements |
<aside>
You mentioned functional programming earlier as well, and this time I will comment on it. ;-) It's not obvious to me why it wouldn't be a bug if you perform a completely unexpected computation, because two entities have the same type, but very different meaning in the application domain, be it ever so side-effect free. Your pure function call would just return a value representing "You were just hit by a bullet!" rather than a value representing "A charming little rose was just born". I do acknowledge, cf. Theorems for Free, that there's some truth to the claim that pure function types are surprisingly informative (e.g., if you know that a function has type But for non-trivial application programming I suspect that there's going to be some overlaps, for example, some functions of type You can use mechanisms like Haskell </aside> ;-)
This wouldn't stop anyone from creating a Still, that might not be easy in practice. But there is still a nominal approach which could be applied: For some time, Dart-the-language used to have a mechanism called 'interface injection'. I believe it was never implemented, and it was already commented out when the language specification was added to the github repository in 2013. I think it was dropped because of serious performance implications. That mechanism would allow a developer to add a subtype relationship 'on the side': class Implicit implements ToJsonable; This declaration could be added anywhere (as long as This would enable developers to say, once and for all, that |
Thanks again for such deep explanation and the reference to 'interface injection'. For now I think we can leave this as is (from my perspective it has been a great and knowledgable exchange). Later on I might come back with a more concrete examples, but it might be that as you suggested, "if you can edit the code where that happens, you could wrap it". I mean it could be convenient not needed to edit the code and it "just works", but on the other hand "just works" with so much magic can come back to the |
How about extending abstract class HasLength { int get length; }
bool isEmpty(dynamic<HasLength> value) => value.length == 0;
class A { int get length => ...; }
class B { int get length => ...; }
main() {
// OK because A and B have the members expected in HasLength
isEmpty(A());
isEmpty(B());
} |
In our case, having structural typing for pure data would be useful. For example... Our backend GraphQL API returns a user entity. This user can contain various fields such as id, name, photo, timezone, email, and so on. Some widgets, such as UserAvatar only require the photo and name while others require a different set of fields. One option is we can declare a giant User object with a lot of nullable fields. (This is the route we went.). However, this is error prone because each property is nullable. Another option is to declare a bunch of classes with permutations of these fields. Such as UserWithTimezone, UserWithPhotoAndEmail, and so on. But the number of "implements" statements to emulate structural typing gets out of hand quickly. |
Consider the following example
The above code throw compile-time error The argument type 'B' can't be assigned to the parameter type 'A', but
class B implements A
it's working. My suggestion is for dart compiler to automatically infer the interface, similar to what Golang does.The text was updated successfully, but these errors were encountered: