language
language copied to clipboard
Should extension structs be allowed to define methods also listed in their interfaces?
In the extension struct proposal (#2360) I allow extension structs to implement class interfaces, in the case that their representation type implements the interface. This allows extension structs to expose the fact that the underlying representation implements an interface. Extension structs are forbidden from providing their own implementation for methods exposed via interfaces in this manner, to avoid the surprising behavior that such methods are statically dispatched, and hence any dispatch of interface methods through the interface will continue to reach the original implementation from the underlying object. Example:
extension struct Nat(int x) implements Comparable<num> {
int compareTo(num other) => -(x.compareTo(other));
}
void compare<T>(Comparable<T> first, Comparable<T> snd) {
print(first.compareTo(snd));
}
void test() {
Nat n = Nat(3);
Nat m = Nat(2);
print(n.compareTo(m)); // prints "1"
compare(n, m); // prints "-1";
}
This is surprising, and it seems to me to be even more surprising than in the extension case (#2369), since rather than extending a struct, we are here claiming to implement a class interface. The primary use of interfaces is to support writing code which is polymorphic over interfaces, allowing declarations to provide their own implementations. With extension structs however, the provided implementations are never used when accessed via that interface. In contrast, extension provides both polymorphism and inheritance, and extension structs are never polymorphic.
cc @mit-mit @lrhn @eernstg @chloestefantsova @johnniwinther @munificent @stereotype441 @natebosch @jakemac53 @rakudrama @srujzs @sigmundch @rileyporter
This continues to feel like a foot gun to me if we allow it. There is almost no reason that I can think of for implementing Iterable<int>
other than to allow the object to be used as an Iterable<int>
. Providing a custom implementation of the Iterable methods in the extension struct is then almost never useful, since any uses as an Iterable will ignore the struct implementation in favor of the underlying representation implementation.
Definite footgun.
Take an object implementing Iterable<int>
. You then "override" get iterator
and think you're clever. Calling any of the exposed instance members will not use the new iterator
because they are doing a typed virtual lookup for iterator
instead.
I worry that even giving extension structs a class-like syntax is a mistake, and the "view" proposal is better because it doesn't look like a class.
There is almost no reason that I can think of for implementing Iterable
other than to allow the object to be used as an Iterable .
Yeah we talked about this for interop mocking, and I agree. If we disallow this, is there any use to having extension structs implement anything?
I think two models are promising:
-
implements
reveals types held by the underlying representation, the extension struct type is a subtype of everything it implements, and overriding a method in an implemented interface in the extension struct is an error. -
implements
is a requirement on the extension struct (so we must have the specified members, with a correct override relation to implemented types), the extension struct is not a subtype of any implemented type, but boxing is supported and the boxed object is an instance of a regular class or struct which is a subtype of the implemented types.
With model 1, overriding is an error, and this means that there is no doubt that invocation of an implemented method will call the method on the underlying representation object.
With model 2, overriding is allowed, and the declaration in the extension struct is faithfully preserved as the implementation which is actually running: Either we use static resolution on an unboxed representation, or we use normal OO dispatch on a boxed representation.
is there any use to having extension structs implement anything?
yes, very much. In my proposal, if the underlying object implements something, you can expose that, and consumers can use it. So for example, you can have a view
on int
, and expose the fact that it implements Comparable<num>
, thereby making the view type usable in places expecting a Comparable
.
@eernstg
2.
implements
is a requirement on the extension struct (so we must have the specified members, with a correct override relation to implemented types), the extension struct is not a subtype of any implemented type,
I'm fairly reluctant to having something that says X implements Y
where X
is not a subtype of Y
. I get that it's just one more step down the path, but I almost feel like I'd rather introduce new syntax if we go this route (and it still feels weird). At its most fundamental level, interfaces are about polymorphism, with conformance only there to validate the polymorphism. That is, we don't generally say Foo implements Iterable<int>
because we feel strongly that Foo
should have an contains
method with a specific signature: we say that Foo implements Iterable<int>
because we wish it to be usable in code which works for any Iterable
. The conformance checking is then secondary to that goal. So it feels a bit off to me to eliminate the polymorphism, while keeping the conformance checking.
but boxing is supported and the boxed object is an instance of a regular class or struct which is a subtype of the implemented types.
In general, this part is something that I still feel is under-motivated. If we do something along the lines of the view proposal, my starting point would be that we not provide this functionality.
my starting point would be that we not provide this functionality.
If we leave out everything about implements
, and we leave out everything about boxing, then we have a simpler design that doesn't create this dichotomy. That certainly makes sense to me.
Alternatively, if we do support boxing then I would expect that we are able to specify and enforce that the boxed object has certain subtype relations, because otherwise the boxed objects wouldn't be very useful. And I do think that it's an inconvenient and substantial reduction of the expressive power of the mechanism, if the boxed object can only have a type which is essentially the same as the type of the underlying representation object.
As a third alternative, consider an extension struct / view S
, with the clause implements T1 .. Tk
and underlying representation object o
with declared type OnType
.
If we support implements
clauses, but restrict them such that Tj
must be a supertype of OnType
for each j
, and we specify that S
cannot have any "overriding" declarations for any of the members of OnType
, then implements
is purely a partial "reveal the representation type" mechanism. In that case we might consider a simpler approach whereby S
is considered to be unrelated to OnType
respectively S
and OnType
are considered to be mutual subtypes. We could do that, e.g., by having open extension struct S ...
vs a plain extension struct S ...
, where the former makes S
and OnType
mutual subtypes, and the latter makes them unrelated. The point is that the implements
clause, in this setting, basically specifies just one bit, so it isn't particularly useful to make it much bigger than one keyword.
I think this is more-or-less settled as "yes", with redeclarations.