-
Notifications
You must be signed in to change notification settings - Fork 207
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
[Inline Classes] Consider allowing inline classes to extend other inline classes #2967
Comments
I am still not convinced that making inline classes look like classes it the best choice. Because they're not classes, they just look like it. Having to declare the representation object as an instance field feels like a loss of abstraction, because it then affects the interface. You have to make the representation value variable private to avoid it affecting the public interface. I'd much prefer a primary-constructor-like syntax which does not introduce a "field", like: @JS()
inline class Element.fromJS(JSObject obj) {
external factory Element();
}
@JS()
inline class DomElement.fromJS(JSObject obj) implements Element {
external factory DomElement();
} where Every inline class would have precisely one primary constructor-like invocation, all further constructor must be factories. If that won't fly, sure, let's allow |
I'm definitely open to alternative proposals.
Fair. This is true of normal classes as well though, so I'm not sure it bothers me much. My general position is that almost all fields should be private, but that's not how Dart works. You mark fields with an
I don't really think this is true. Without extends, you have to respecify the representation somehow. In this proposal, you do so by declaring a field. In other proposals you do... something else, but you have to do it.
So basically, you want to make inline class fields private regardless of name. That's fine, but... it's yet another divergence from other classes that you have to learn. We can do it (I've proposed making them final by default as well), but the delta between this and just declaring it as
Your proposal above doesn't eliminate the annoying redundant instance fields, right? You still have to re-specify in each class |
TL;DR: I don't mind the redundancy, only how it looks. Using inline class MyNum {
final num _value;
MyNum operator^(int other) => pow(_value, other) as MyNum;
}
inline class MyInt implements MyNum {
final int _value;
MyInt operator^(int other) => pow(_value, other) as MyInt;
} Here I cannot use I guess I just don't have an issue with redundancy in declaring the representation type independently for each inline class, My issue is that adding the "field" as a getter to the interface links the underlying object and the API on top of it, and that's two abstraction layers I'd prefer to keep separate. That's why I say "leaking abstraction", because you are forced to leak the underlying type in the overlay API. Sure, you can make it private so nobody else can see, but it's still mixing the levels of abstraction. And if you don't have the same name as the superclass, it gets even harder to explain. @JS()
inline class Element {
final JSObject obj;
Element.fromJS(this.obj);
external Element();
}
@JS()
inline class DomElement implements Element {
final JSObject elementObject; // I liked this name better.
DomElement.fromJS(this.elementObject);
external DomElement();
} Here you'd inherit the JSObject get obj => this as JSObject;
JSObject get elementObject => this as JSObject; Saying that an inline class can only have one field, and then still looking like two is confusing. (I have a very hard time saying precisely what irks me, but it's the "square peg, round hole" feeling which usually means headaches will follow in the future.) |
Fair enough. Some of our early prototype users very much do find the redundancy bothersome though, hence this issue.
I don't really find it confusing. There are two getters that return the same value, possibly at different types. But it is a bit messy, I agree. As I say, I'm open to alternative suggestions (probably in a different issue though). We just haven't found anything else yet that addresses all of the issues in totality as this formulation. |
ACK. Separate issue. Using It solves the problem of repeating the same representation type variable, and nothing more. But that's still a win. So 👍 to that within the current model. (Is there more we should add too, for consistency? We should still extend |
We can very easily get rid of the representation field: Just go back to an approach where we specify the representation type using However, I do think we gained something useful by switching to a model where an inline class is similar to a class with exactly one instance variable, and it is basically an implementation detail that there is no ordinary object holding that instance variable: There's a complete set of features supporting construction of the representation object, using normal generative constructor declarations whose semantics matches that of normal classes so closely that developers don't have to think about it. For example, this allows us to use an inline class as a value class: // Wanted:
value class Point {
final int x, y;
Point(this.x, this.y);
... // Various members.
}
// OK, use this:
inline class Point {
final (int, int) rep;
Point(int x, int y): rep = (x, y);
int get x => rep.$1;
int get y => rep.$2;
... // Various members.
} This inline class emulation of a value class yields strictly enforced immutability ("inherited" from the underlying record), structural equality, lack of identity (that is, giving the permission to box and unbox freely in the implementation), and sound covariance. This means that it is a quite good approximation of designs that we've discussed under names like 'value classes'. The representation type can leak, and the syntax of the declaration is not as concise as a real built-in Based on that, I'd prefer to keep the model where an inline class is very similar to a regular class with a single instance variable, and then we may consider some abbreviated forms covering frequently used cases. Taking inspiration from class Inline<X> {
final X rep;
const Inline(this.rep);
} and the unique instance variable would then be introduced by regular inheritance: // `on String` means `extends Inline<String>`, but we probably
// won't support an explicit `extends` clause.
inline class Foo on String {
Foo(super.rep);
} This would work for the inline class Point on (int, int) {
Point(int x, int y): super((x, y));
int get x => rep.$1;
int git y => rep.$2;
... // Various members.
} We could say that the default constructor (the one you get if you don't declare any generative constructors) has the form inline class IdNumber on int {
// Implicitly gets `IdNumber(super.rep);`.
} This means that we could get a standardized name (everybody knows that it's called We might want to make |
I would like to suggest to choose an easier to read and remember, unabbreviated, standard name for code examples instead of rep, such as value. |
Right, we do have a general rule that identifiers should not be abbreviations. However, we've had an issue for about 6 years about Also, 'representation' is a direct reference to the phrases 'representation type' and 'representation object', which are meaningful interpretations of the role played by the unique instance variable in an inline class (the inline class provides a given representation object with a new static interface). That's the reason why I'd like to have some connection to the word 'representation'. Nevertheless, we can always use a thousand proposals and see if something is both informative and short. |
My quick 2 cents: // `on String` means `extends Inline<String>`, but we probably
// won't support an explicit `extends` clause.
inline class Foo on String {
Foo(super.rep);
} I think the reference to I really like the final goal of saying that there's only a representation type visible, and if you want, you can access the field through a standardized name. I also like How would this work with |
If we go with The advantage of the class-look-alike declaration is that we have existing syntax to flow the value into the variable (constructors). The similarity with classes breaks down if we start leaning into the "it's really the representation type with a view" perspective. inline class MapEntry<K, V> implements ({K key, V value}) {
final ({K key, V value}) _entry;
MapEntry(K key, V value) : _entry = (key: key, value: value);
} where the The relation between inline class and representation type is not a relation we have anywhere else. It's like a subtype relation, because it really is an "is-a" relation, but it's not necessarily reflected as such in the declaration. |
I don't think the Say we have an inline class inline class Inline<X> {
external Inline(X representationValue);
} and say that all other inline classes have to extend another inline class, then If we don't give the representation value a name, we can't access it, which is annoying. Giving it a public name clutters the API and would be highly annoying. Let's use inline class Inline<X> on X _ {
external Inline(X _);
} Then you can subclass by doing: // Implicit `extends Inline<String>`.
inline class MyString on String base { // <- this declares the representation type and private access name.
MyString(super.base); // This initializes the value by calling `Inline<String>(...)` as super-constructor.
// ... members, can access `base` lexically only.
}
// Inherits all `MyString` members.
inline class MySuperString extends MyString on String base {
MySuperString(super.base);
// more members.
} Still feels cumbersome. OK, lets try another syntax, with primary constructor-like notation: // On String and export string.
inline class MyString(String base) extends String {
// members, access base only lexically.
}
// Exposes inherited MyString and String members.
inline class MySuperString(String base) extends MyString {
// More members. Can shadow.
} You can still declare constructors, but no other non-redirecting generative ones. They all have to forward to the primary constructor (or cast). inline class MySuperString._(String base) extends MyString {
factory MySuperString(String base) {
if (base.isEmpty) throw ArgumentError.value(base, "base", "Must not be empty");
return MySuperString._(base);
}
factory MySuperString.tricky(String base) => base as MySuperString; // look, no constructor!
} That's my proposal: inline class Name<TypeParameters>.nameOpt(parameter) implements TypeList {
... members but not non-redirecting generative constructor or instance variables ...
} The The primary constructor-like syntax allows the constructor to be named, and therefore private. Or not. The primary constructor-like syntax allows precisely one parameter. The constructor is almost trivial. It does no validation, no super-constructor invocation, no nothing. The
The inline class type is a subtype of every type in An instance member declaration can be abstract. If so, it implicitly forwards to the representation type member with the same name, and the declaration in the inline class must be a supertype of the method signature it forwards to. |
@eernstg This issue discusses ease of syntax and being able to roll an on-type into an |
No, that idea has been around for a long time, but there is no dedicated issue. I just created this one: #3090.
Thanks! I did want to put some of the ideas that we have had on the table into that issue, so I wrote #3090, but that's of course just a place where discussions about any and all such ideas can be taken. |
@eernstg what does the code from #2967 (comment) look like if we settle on #3182 ? |
Like this: @JS()
extension type Element.fromJS(JSObject obj) {
external Element();
}
@JS()
extension type DomElement.fromJS(JSObject obj) implements Element {
external DomElement();
} |
This is more-or-less settled and implemented , yes? |
Not currently planned. |
Early feedback from users of the inline class prototype has called out that for certain use cases, there is a fair bit of unnecessary boilerplate associated with the current mechanism. For our javascript interop efforts, the representation field is never accessed in the inline class, and yet every subtype of an inline class needs to repeat the same representation type and field. Example:
We could consider allowing an inline class to extend another inline class, thereby "inheriting" the representation object of the super class.
We would presumably still allow other inline classes to be used in an
implements
clause. The class listed in theextends
clause is distinguished as the super class from which the representation field is chosen.cc @dart-lang/language-team @joshualitt @srujzs @sigmundch @eernstg
The text was updated successfully, but these errors were encountered: