dart-lang / language

Design of the Dart language
Other
2.61k stars 200 forks source link

Confusion with extension types and `implements` #3463

Open water-mizuu opened 8 months ago

water-mizuu commented 8 months ago

First, I apologize in advance in case I misunderstand the meaning of implements in extension types. However, I find that it is confusing, considering how implements work in classes. This is mainly due to the fact that in classes, implements means that a class must follow a specified interface or "shape" of an object. However, in extension types, implements (I think) means "to expose" the interface of the underlying object. For example:

extension type const ImmutableList<T>(T _) implements Iterable<T> {
    T operator[](int index) => _[index];
    int get length => _.length; /// This is not necessary because of the implements clause.
}

So what's the idea with implements? Is it supposed to expose the underlying API of the object? Is it supposed to ensure that the object obeys its superinterface by exposing needed elements?

lrhn commented 8 months ago

The implements of an extension type is not precisely the same as for a class or mixin declaration. It has a specific meaning for extension types, it's just reusing an existing keyword. (Other words were considered, but implements won in the end.)

The effect of implements Foo of an extension type Ext(...) implements Foo {...} declaration is:

The first item is as simple as it sounds. Being a subtype means all the usual things, including assignability. An extension type is not a subtype of any non-top type unless it declares so using implements.

The member signatures added to the type signature of MyInt, possibly merged with other member signatures of the same name from other super-interfaces if the extension type has multiple superinterfaces, will be satisfied by the representation type, and member invocations will be forwarded to the representation object. Because of that, an extension type can only implement an interface if it's guaranteed to also be a supertype of representation type, so the representation object is guaranteed to have a valid implementation.

That is:

extension type MyInt(int _) implements int {
  String operator/(String other) => other.substring(0, _);
}

introduces a new type, MyInt. That type is a subtype of int (MyInt \<: int), because it says so. It's only allowed to do that because int is a supertype of the representation type int (very trivially).

The, for example, operator- member signature of int is added to the type signature of MyInt, with the same member signature as in int: num operator-(num other). Invoking that member will invoke the same member on the representation object, at the representation type, so:

var mi = MyInt(63);
print(mi - 21); // prints 42

is valid. The mi - 21 refers to the operator- of MyInt, which is inherited from int and the mi - 21 will, at runtime, invoke 63 - 21 directly.

In comparison, the operator / of MyInt is the one declared by MyInt. The operator / from int is just not included in MyInt.

bwilkerson commented 8 months ago

@MaryaBelanger For the confusion and explanation.

water-mizuu commented 8 months ago

It makes the Ext type be a subtype of the Foo type.

This makes sense, as for the natural function of an implements clause,

It makes every member of the Foo type signature also be a member of the Ext type signature, unless shadowed by a declaration of Ext.

I think that implements is a bad choice for this, considering beginners who would be confused by two different applications of the same keyword in two different contexts. However, this could also just be a result of me not being used to it yet and would eventually get over it.

I would understand that this aligns more with extends since it basically "inherits" the signatures from Foo, but since it allows shadowing to a different type, it's technically not the same.

lrhn commented 8 months ago

Another perspective, which may just be an attempt to rationalize, is that implements on an extension type really does only add the abstract member signatures of the implemented interface, just like it does on classes.

The implementation is already there, on the representation object, it's just not visible in the extension type signature.

Because an extension type cannot be abstract, you're only allowed to implement an interface that your representation type has implementations for, but the signature added by implements doesn't have to match the implementation precisely, it can be that of a supertype. (And if we add the feature of allowing you to write abstract members directly, which would be forwarded to the matching representation objects method, and not only get such abstract members through implementing interfaces, then it would still match this view.)

water-mizuu commented 8 months ago

Another perspective, which may just be an attempt to rationalize, is that implements on an extension type really does only add the abstract member signatures of the implemented interface, just like it does on classes.

The implementation is already there, on the representation object, it's just not visible in the extension type signature.

I see. Then what about exposes as the keyword instead of implements? I can understand the reasoning behind implements now, but with how you showed it, I would think exposes would be the most appropriate one for extension types. Moreso since the clause only allows the type itself or its supertypes.

extension type const ImmutableList<T>(T _) exposes Iterable<T> {
    T operator[](int index) => _[index];
    int get length => _.length; /// This is not necessary because of the implements clause.
}
lrhn commented 8 months ago

The word exposes is a completely new keyword, which means it carries no baggage. That's both good and bad.

I think implements or extends are the only two viable existing keywords. We tried both. It was considered too confusing that you could have multiple super-extension-types using extends Foo, Bar. So implements was chosen.

It has grown on me, so today I actually think it's a good choice.

Using an unrelated keyword, not usually used in declarations, is an option. Something like be super or is. But I still think they're too far from what the meaning is. (Especially if I can get #3381, then implements is precisely right.)