language icon indicating copy to clipboard operation
language copied to clipboard

Should extension structs be allowed to define methods also listed in their interfaces?

Open leafpetersen opened this issue 1 year ago • 8 comments

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.

leafpetersen avatar Jul 29 '22 23:07 leafpetersen

cc @mit-mit @lrhn @eernstg @chloestefantsova @johnniwinther @munificent @stereotype441 @natebosch @jakemac53 @rakudrama @srujzs @sigmundch @rileyporter

leafpetersen avatar Jul 30 '22 00:07 leafpetersen

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.

leafpetersen avatar Jul 30 '22 00:07 leafpetersen

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.

lrhn avatar Jul 31 '22 10:07 lrhn

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?

srujzs avatar Aug 01 '22 21:08 srujzs

I think two models are promising:

  1. 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.
  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, 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.

eernstg avatar Aug 01 '22 22:08 eernstg

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.

leafpetersen avatar Aug 03 '22 20:08 leafpetersen

@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.

leafpetersen avatar Aug 03 '22 21:08 leafpetersen

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.

eernstg avatar Aug 04 '22 12:08 eernstg

I think this is more-or-less settled as "yes", with redeclarations.

srawlins avatar Sep 26 '23 17:09 srawlins