-
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
[extension-types] Automatically provide access to some members of the representation interface #2506
Comments
This feature would be awesome for composition (optionally with Dependency Injection). This allows abstracting the dependencies out of a class and achieve inversion of control. @eernstg Could we "mimic" inheritance like this? I like the idea of achieving composition with interfaces. Or would this be a completely horrid idea? // maybe change to interface class in future dart, but if we want common behavior an abstract class is also nice
abstract class WeatherService {
double fetchTodayTemperature();
}
class AccuWeatherService implements WeatherService {
@override
double fetchTodayTemperature() {
// fetch from AccuWeather
return 20.0;
}
}
class GoogleWeatherService implements WeatherService {
@override
double fetchTodayTemperature() {
// fetch from Google
return 21.0;
}
}
class WeatherControllerUsingCompositionAndDI implements WeatherService {
WeatherService weatherService;
// export weatherService show WeatherService;
// or
// Some syntax for single line
// export WeatherService weatherService show WeatherService;
//@inject (AccuWeatherService or GoogleWeatherService)
WeatherControllerUsingCompositionAndDI(this.weatherService);
}
class WeatherControllerUsingComposition implements WeatherService {
WeatherService weatherService = AccuWeatherService();
// export weatherService show WeatherService;
}
class WeatherControllerSimple {
WeatherService weatherService = AccuWeatherService();
// export weatherService show (WeatherService or *);
} With this approach we can type Inheritance still got it's place no doubts, but this way we can easily achieve composition over inheritance mantaining the type polymorphic with the advantages of composition. |
@rubenferreira97 wrote:
It wouldn't work out of the box. In particular, there's a big difference between method forwarding and message delegation known as the 'Self problem'. The core of this problem is that if an object class A {
void m() => n();
void n() => print('A.n');
}
class B extends A {
void n() => print('B.n');
}
void ForwardingB implements A {
final A a = A();
void m() => a.m(); // Just forward every method invocation to `a`.
void n() => print('ForwardingB.n'); // Except that we want to override `n`.
}
void main() {
A a = B();
a.m(); // Prints 'B.n'.
a = ForwardingB();
a.m(); // Prints 'A.n', not 'ForwardingB.n'.
} Given that overriding doesn't work, I'd be very careful about recommending that inheritance is replaced by forwarding. It is true that forwarding is more flexible than inheritance (especially if we're forwarding to a mutable variable), but I wouldn't consider it to be a viable approach in general. Also note that the static analysis gets quite tricky in the case where the language supports true delegation (that is, a variant of forwarding where the Self problem has been solved in the language itself). See, for example this paper. |
Maybe the expression "mimic inheritance" was not the best choice of words because it can lead to a wrong interpretation of what I was asking (that paper seems very interesting but a completely different thing. I will take a deeper look). I don't know how should I rephrase it tho, interfaces concretized through (composition) forwarding? I kinda understand what you see as a In your example I would see this as syntactic sugar: class A {
void m() => n();
void n() => print('A.n');
}
// Forwarding implements A so we need to comply with it, we don't care how, it's an interface
void Forwarding implements A {
// Syntax sugar for `void m() => a.m();` // Just forward m invocation to `a`.
export final A a = A() show m;
// If we declare both `show m` and `void m` it's a duplicate, compile error
// void m() => print('Forwarding.m'); // compile error
// "Except that we want to override `n`." -> No, we want to enforce interface A. How that's decided it's up to the developer.
void n() => print('Forwarding.n');
}
// ForwardingAll implements A so we need to comply with it, we don't care how, it's an interface
void ForwardingAll implements A {
// Syntax sugar for `void m() => a.m();` and `void n() => a.n();`
export final A a = A() show (* or A);
void methodThatOnlyForwardingAllHas() => print('Hi!');
} It's verbose to forward each method. This simply would allow developers to forward any method with a better granularity. |
You could say that, but I tend to think that forwarding should be considered to be a specialized technique which must be used with a lot of caution, rather than a better way to express inheritance. Consider the perspective that it could be a recommended approach to stop using inheritance, say, from a class The problem is that everybody who's maintaining code like that would need to know that there should never be an overriding declaration. As soon as any one of those classes-using-composition-to-emulate-inheritance implements one or more of the methods of the forwardee rather than forwarding, we're looking at a subtle bug. class Animal {
void live() { ... eat(); ... }
void eat() {
print('eating in a very generic manner');
}
}
class Cat implements Animal {
final animal = Animal();
export animal as Animal hide eat; // We can say `hide`, so why not?
void eat() {
print('open the mouth dramatically with each bite and say grrrrrrrrrr!');
}
}
void main() {
Animal a = Cat();
a.eat(); // Fine, says grrrrrrrrrr!
a.live(); // Oops, when it eats, it's done in a generic manner, no drama!
} If we want to override anything then we'd basically need to switch back to using inheritance (so we might need to change an entire class hierarchy). I consider that to be a strong hint that "use composition rather than inheritance" is a questionable piece of advice, or at least error prone and hard to handle. That said, I still think forwarding is sufficiently useful to come up with a proposal like this issue. 😀 It is particularly useful with inline classes, because they never give rise to polymorphism: If you're calling an inline class member then it's because the static type of the receiver is that inline type or a subtype (which is another inline type), so we never have a situation where "the member that we should have invoked" is different from the one that we know statically at the call site. |
I really appreciate your thoughtful answer. It opens up a new view of thinking, but also problems that I might not have considered. I am not saying we should replace inheritance when it seems the "correct way" to tackle the problem. I am, myself, more of a fan in using "the right tool for the job" rather than "use composition over inheritance" everytime. However I still think this is not a problem with forwarding, more like using composition wrongly. class Animal {
void live() { eat(); }
void eat() {
print('eating in a very generic manner');
}
}
class Cat implements Animal {
final animal = Animal();
@override
void live() => animal.live();
@override
void eat() {
print('open the mouth dramatically with each bite and say grrrrrrrrrr!');
}
}
void main() {
Animal a = Cat();
a.eat(); // Fine, says grrrrrrrrrr!
a.live(); // Oops, when it eats, it's done in a generic manner, no drama!
} You can still find this "bug" today. I only view forwarding as syntax sugar, the problem is already there to begin with 😉. |
+100 for that tool thing! ;-) About "composition over inheritance", I'm somewhat worried about this motto being used as a general rule, in particular when it overrides the rule about "the right tool for the job". For example, Wikipedia: Composition over inheritance clearly describes "composition over inheritance" as a general principle. I believe it's fair to say that this article does not at all emphasize that composition may be a suitable tool in some cases, and inheritance may be a suitable tool in other cases, and those cases have a completely different conceptualization: The former fits a "has-a" relationship and the latter fits an "is-a" relationship, and those two things are very, very different. The Wikipedia page does say that
but that's the only time it mentions 'has-a' and 'is-a', and it seems to suggest that we can freely reorganize any given class across that distinction, and we should really turn as much as possible into the 'has-a' bucket. I believe this can only be done (without breaking all clients) if we introduce forwarding methods for all the 'has-a' parts, and then it gets a lot more blurry whether it's still a 'has-a' relationship. Conversely, if we don't have forwarding methods then there's no Self problem, and having a (genuine) 'has-a' relationship is one of the fundamental and crucial tools in the OO toolbox, no problems with that. I think I've learned the following from this discussion: A feature in Dart that supports concise generation of sets of forwarding methods (let's say we are using syntax like This lint should flag every instance member declaration D named class A {
void m() => n();
void n() {}
}
class B implements A {
final A a = A();
export a show A;
}
class C extends B {
void n() {} // Lint!
} |
I prefer the original syntax with show / hide Also what's the reason to hide by default ? Personally I would have
|
@cedvdb wrote:
That is certainly a point that could be argued. However, here are a couple of reasons why I tend to think that an export mechanism fits rather well in the class body: First, an export directive in a class body is a concise notation for a set of concrete member declarations in the class body. It should be an implementation detail for a client whether a class In any case, IDEs should probably show all the implicitly induced members when the user is hovering on an export directive. This makes it reasonable to think about an export directive as a "folded" set of declarations. Next, the export directive is able to establish a forwarding relation to different targets, and they would (typically) be looked up in the class body: class A { void m1() {}}
class B { void m2() {}}
class C {
final A a = A();
final B b = B();
export a; // Implicitly induces `void m1() => a.m1();`.
export b; // Implicitly induces `void m2() => b.m2();`.
} It would give rise to a surprising treatment of scopes if we used something like
Earlier proposals had different variants, but the inline class feature which has been accepted by the language team makes the inline class type and the representation type unrelated. This is useful in the case where the inline class is used to establish a certain compile-time discipline on the use of an object. For instance, if we have I proposed that we could offer a very different set of defaults using a different declaration syntax, here. The idea is that an In other words, I certainly agree that different combinations of defaults are worth considering. It's just that "don't export any representation type members (other than |
Why do you want that ? Typically, I'm just looking to wrap primitives with domain concerns and this is the kind of things I'm looking for : inline class Username(String _value) {
// ...
bool get isValid() => // ...
}
final message = "hello eernstg";
final username = Username("eernstg");
print(message.endsWith(username));
print(username.endsWith(message));
// but have a compile time error here
inline class Description (String _value) {}
updateDescription(Description description) {}
updateDescription(username) {}
// flutter
Text(username) So
Where would that information leak to the consumer ? I assume the IDE would not show to the consumer any of that info, and if it did it could be misleading
Wow this is totally different than the original @DelegatingFacade
class MyClass {
@Delegate() final A _a;
@Delegate() final B _b;
} |
Because addition of ID numbers is basically guaranteed to be a bug, and it could be useful to (1) use a cheap/fast representation and (2) get a compile-time error each time we happen to write code where that bug occurs. Another example could be a very simple kind of unit management: inline class Liter {
final double value;
Liter(this.value);
Liter operator +(Liter other) => Liter(value + other.value);
Liter operator -(Liter other) => Liter(value - other.value);
Liter operator *(double other) => Liter(value * other);
}
void main() {
var vol1 = Liter(10), vol2 = Liter(5);
var volSum = vol1 + vol2, volDiff = vol1 - vol2;
var volScale = vol1 * 10;
// Compile-time error.
// vol1 * vol2;
} This will reduce to simple operations on You could do the same thing using an actual wrapper object (just delete the word The trade-off is real: Do you want the full-fledged object behaviors that a regular wrapper object will give you? Or do you want the zero-cost abstraction of an inline class? The former will incur a time/space cost at run time, the latter uses compile-time-only encapsulation which can be violated by a cast. Of course, if you want those static checks you should just avoid having casts away from inline types.
Check out #3090. You would use inline class Username(String _value) implements String {
// ...
bool get isValid() => // ...
}
inline class Description (String _value) {} // May or may not have `implements String`.
void updateDescription(Description description) {}
void main() {
final message = "hello eernstg";
final username = Username("eernstg");
print(message.endsWith(username));
print(username.endsWith(message)); // OK.
// Original code: `updateDescription(username) {}`.
// I assume this was intended:
updateDescription(username); // Error, `Username` is not assignable to `Description`.
// flutter
Text(username); // This would be OK when `Username implements String`.
} If you want more control over forwarding behavior you could consider an
We would use inline class Username(String value) : shows String hides toUpperCase { // showing this part to consumer
String toUpperCase() => value.toUpperCase();
} So I'd prefer this one: inline class Username(String value) implements String {
String toUpperCase() => value.toUpperCase();
} We don't even need an (Yes, removing a member in a subtype is a new mechanism; you could say that it works like a declaration that has no implementation and just indicates that it is a compile-time error to call this member on this receiver type. This came up because there was a discussion about using an inline class
That won't quite do it: abstract class A {
void m([double i]);
}
class B1 extends A {
void m([double i = 1]) {}
}
class B2 extends A {
void m([double i = 2]) {}
}
const maybeOneAndAHalf = 1.5;
class C {
final A a;
C(this.a);
void m([double i = maybeOneAndAHalf]) => a.m(i); // Not a faithful forwarding.
} So we need a little bit of language magic in order to be able to write a forwarding function which will faithfully preserve the behavior of a direct call to the forwardee, and you can't generate Dart code to do that. |
This feature will not be included in the first version of extension types: re-labeling as 'extension-types-later'. |
[Edit: The feature name 'view class' has been changed to 'inline class', then to 'extension type'. Adjusted the text and code below accordingly.]
During the development of the extension type feature, we had several ways to declare concisely that the interface of the given extension type should include specific members of the interface of the underlying representation type. For example:
This would compile without errors because the type of
v.successor
isV
, and theexport
declaration adds the specified members (in this case just one member:isEven
) to the interface ofV
, with the signature taken from the interface ofint
.A more practical perspective on this mechanism would be that
export
is a request for automatic generation of a set of forwarding methods (with the same signature as the selected members of the interface of the representation object). Another perspective is that invocations of members in the set of members exported by anexport
declaration are handled by considering the receiver as having the representation type. That is,v.successor.isEven
is desugared tov.successor.i.isEven
.In any case, the semantics of the forwarding method is that it works as if every invocation were an invocation of the forwardee with the same actual arguments (in particular, we get the same default values of optional parameters).
The mechanism as proposed supported
show
andhide
clauses as well as the use of identifiers denoting member names,<type>
s denoting sets of member names,get <identifier>
andset <identifier>
denoting just the getter or just the setter. Special considerations arose for the names of members ofObject
; for instance, you probably wanthide num
to mean that all members of the interface ofnum
are removed, except for the members ofObject
. And so on, lots of stuff to consider.In any case, it seems useful to be able to select any subset of the members provided by the representation type and make them available on the extension type.
Note that the perspective where the
export
declaration is understood as a way to implicitly induce a set of forwarders will immediately generalize to other class-like declarations:This issue serves to keep in mind that we might still want to add a mechanism like that.
The text was updated successfully, but these errors were encountered: