Skip to content
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

Should extension structs support sub-classing? #2368

Closed
Tracked by #2360
leafpetersen opened this issue Jul 29, 2022 · 10 comments
Closed
Tracked by #2360

Should extension structs support sub-classing? #2368

leafpetersen opened this issue Jul 29, 2022 · 10 comments
Assignees
Labels
data-classes extension-types inline-classes Cf. language/accepted/future-releases/inline-classes/feature-specification.md structs

Comments

@leafpetersen
Copy link
Member

leafpetersen commented Jul 29, 2022

In the extension struct proposal (#2360), I propose that extension structs should be forbidden from extending other extension structs. At the end of the proposal, there is a discussion of allowing this. This issue is for discussion of whether to allow this, and with what constraints and semantics.

@leafpetersen
Copy link
Member Author

@srujzs and @sigmundch have indicated fairly strongly that this is close to a requirement for the JS interop use case, so this discussion may be somewhat pro-forma. Overall, I see almost nothing objectionable to allowing inheritance without overriding. The discussion of overriding is tracked separately in dart-lang/sdk#2369 .

There is one concern around allowing inheritance across libraries, not because of extension structs, but because of structs. Discussion of this for structs is tracked in dart-lang/sdk#2367 . We of course do not need to be consistent between extension structs and structs here, but it's slightly odd if we don't. This might push towards splitting the two features further apart (e.g. into the data class and view class syntax) to reflect the fact that the semantics diverge.

@lrhn
Copy link
Member

lrhn commented Jul 31, 2022

What does it mean to extend a view/extension struct. Presumably it means that you include the extended view's methods and add your own on top (whether overriding or not).

That sounds safe.

The big question is whether the extension struct types has a subtype relation.
If they do, what does that mean?

  • Assignable from subtype to supertype.
  • ???

That doesn't seem particularly valuable, since you can always cast back to the underlying viewee type and back into the supertype. It's the type of the underlying object which really matters for which assignments are valid, not the extension struct types.

@eernstg
Copy link
Member

eernstg commented Aug 1, 2022

The subtype relationship may be more convenient, to such an extent that it matters:

extension struct Node(JSObject that) {...}
extension struct Element(JSObject that) extends Node(that) {...}

void f(List<Node> nodes) {...}

void g(List<Element> elements) {
  ... f(elements) ... // OK when `extends` implies subtyping.
  // As opposed to:
  ... f(elements as List<Node>) ... // Required when not.
}

In particular, we may get as expressions wrong, without getting any heads up from the analyzer or compiler, because we are unable to specify that this particular cast is from an extension struct type to another extension struct type that happens to be similar (and the type system doesn't even recognize that they are similar at all).

@leafpetersen
Copy link
Member Author

The big question is whether the extension struct types has a subtype relation.
If they do, what does that mean?

My goal with this proposal is to minimize surprise for consumers of the API. With that in mind, I had the design goals in mind that I listed here. Specifically for extension, I think a consumer will be very surprised if A extends B and A is not a subtype of B, and if A does not have a superset of the methods of B. So I have set this up so that this is always the case.

@sigmundch
Copy link
Member

Rocking the boat a bit here - I'd like to revisit our requirements from JSInterop and see if "extension/subclassing" is the right model here.

Some alternatives to think about:

I believe most of our goals around subclassing are really about having the ability to selectively forward nonvirtual methods. By selectively here I mean that sometimes we want to forward all, sometimes some and hide others, and sometimes we want to shadow some.

Note: the fact that we need to hide APIs brings already a semantic difference from the traditional extends: the set of methods in the subclass is no longer guaranteed to be a superset of the methods in the superclass. That is enough for me to require using a different syntax here (similar to the issues raised by @lrhn with implements in #2363 (comment))

One proposal that comes to mind is to explicitly introduce the concept of forwarding nonvritual methods. For example:

extension struct B(int x) {
  get isOne => x == 1;
}

extension struct A(int x) forwardsto B {
  // the forwardsto gets expanded into:
  // get isOne => B(x).isOne
}

Such forwarding concept will allow us to handle several of our use cases:

  • We can use it to model inheritance in the DOM (e.g. extension struct HtmlElement(JavaScriptObject node) fowardsto Node)
  • We can use it to implement shadowing libraries, such as:
    • A shim that translates old dart:html APIs to new JSInterop based APIs. (Note that this requires hiding some APIs or shadowing them with a new signature (e.g. return type will not match)).
    • A safe-html layer that can enforce stronger security standards than the low-level JSInterop APIs (this too may require shadowing, for example, an API takes a SafeURL or a SafeHtml instead of a raw String).

Also worth noting that some of these use cases sometimes introduce the need to shadow multiple classes in a class hierarchy together, which creates non-traditional multiple inheritance relationships. For example:

// library 1 defines:
extension struct Node(JavaScriptObject x) {}
extension struct HtmlElement(JavaScriptObject x) forwardsto Node {}
// library 2 defines:
extension struct Node(JavaScriptObject x)  forwardsto library1.Node /*with some shadowing */ {}
extension struct HtmlElement(JavaScriptObject x) forwardsto library2.Node, library1.HtmlElement /* with some shadowing */ {}

That said, this kind of shadowing of class hierarchies may be too specific to one single use case of ours. As such, we would be OK if we decide this is not something we will design for. If needed, we can address all the shadowing through codegen instead (it would mean though that we replicate every definition and manually do the expansion I presented earlier with "forwardsto", though)

@lrhn
Copy link
Member

lrhn commented Aug 10, 2022

What if we introduced method forwarding in general, in a way that also applies to classes.

class Something implements Foo {
  FooBar x;
  // ...
  export x {  // or whatever keyword.
    foo, 
    bar, 
    operator+, 
    interface Foo,
  }
}

which means that the Something class exposes the member signatures of FooBar.foo, FooBar.bar, FooBar.+ and all the member signatues of the interface Foo (which FooBar must implement), and forwards all those to the same member on x (unless otherwise implemented in Something).

The Something class doesn't have to implement Foo for this, it can expose the members anyway.

The expression after export is a full expression, it's evaluated each time a member is forwarded. Typically it'll just be a getter invocation.

Shorthands:

  export e;  // same as `export e {interface <static type of e>}`

(Getters/setters may be a problem, if export x {foo} exports only the getter, then export x {foo, foo=} gets verbose and repetitive. If export x {foo} exports both getter and setter, then there is no way to only export the getter.)

Since each forwarding is independent, you can forward extension/non virtual methods as well, it all depends on the static type of the exported expression.

This can then be used in any declaration, whether it's a class, mixin, struct or view. It's just shorthand for writing forwarders,
so export this.list {add} just means void add(X value) => this.list.add(value);, which is a valid way to declare a method in any context.

(Hmm, that means we can even do static export e { ... }. Probably not top-level exports, that would conflict with actual exports ☹️.)

@sigmundch
Copy link
Member

I do like that. and the export syntax is growing on me. I could even imagine using the show/hide as well:

class A {
...
  export e 
     show add, remove;
  export f 
     hide foo;
}

@natebosch
Copy link
Member

What if we introduced method forwarding in general, in a way that also applies to classes.

I like this syntax. This would cover my use case in #3748

@eernstg
Copy link
Member

eernstg commented Sep 22, 2022

Everything about the export declaration has been deleted from the most recent version of the view specification. It's certainly possible that we will introduce that kind of mechanism in the future (on views, classes, mixins, and whatnot), we just need to reserve some time to get it right.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
data-classes extension-types inline-classes Cf. language/accepted/future-releases/inline-classes/feature-specification.md structs
Projects
None yet
Development

No branches or pull requests

5 participants