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

Views: Using non-virtual methods as a basis for view classes #2402

Closed
leafpetersen opened this issue Aug 10, 2022 · 6 comments
Closed

Views: Using non-virtual methods as a basis for view classes #2402

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

Comments

@leafpetersen
Copy link
Member

In discussion with @sigmundch @rileyporter @srujzs and @joshualitt this afternoon, we discussed basing the view feature around non-virtual methods. This provides a fairly clean semantic model that potentially meets most requirements, but it's not clear how to build it out, and it has a lot of surface area. This issue is not to make a full proposal, but to provide a venue for discussing the idea, and seeing if it's worth trying to flesh out into a proposal.

Non-virtual methods.

Suppose we added non-virtual methods to classes (ignore views for now). To start with, let's assume that we simply mark methods as nonvirtual.

class A {
  nonvirtual int foo() => 3;
}
class B extends A {
  nonvirtual int foo() => 4;
}


void test() {
   B b = B();
   print(b.foo()); // Prints "4"
   A a = b;
   print(a.foo()); // Prints "3"
}

There are some questions about exactly what we require.

Do we require compatible signatures in subclasses (we don't have to).

class A {
  nonvirtual int foo() => 3;
}
class B extends A {
  // Is this legal, or an error
  nonvirtual String foo() => "4";
}

We could make this an error, or allow it, I think - either is valid. Extension methods would allow it.

Do classes "inherit" nonvirtual things that they don't override (the way that they inherit extension methods), or are nonvirtual methods only available on the interface that they are defined in?

class A {
  nonvirtual int foo() => 3;
}
class B extends A {}
class C implements A {}

void test() {
  B().foo(); // Works, or static error?
  C().foo(); // Works, or static error?
}

If we view these as analogous to extension methods, both should work, but we don't have to take that model. There are lots of questions about how we would resolve conflicts, and about how we would ensure that this calls in the body of "inherited" methods continued to reach valid method targets.

Applicability to views

We can now start by saying that a view is (in part) a class which is required to only have nonvirtual methods, with the following implications:

  • A view can extend another view, but there are no virtual methods to inherit, so it's largely pointless.
  • A view can implement any other view whatsoever, since the interface of a view is always empty
    • If we use the "extension method" semantics from above where subtypes by default get the supertype nonvirtual methods, then this is modulo some issues around resolving conflicts as described above.

So a view is a restricted class which:

  • May only have one field (or alternatively, we use the "on type" syntax)
  • Must have only non-virtual methods
  • Can only implement things, can't extend or mixin

For the interop use case, this covers the following needs/wants:

  • We can build up arbitrary subtype hierarchies among views
  • We have statically dispatched methods tied to a nominal type
  • We can override methods from parent views

If we take the extension method semantics approach (where nonvirtual methods on the supertype apply to the subtype unless there is a more specific nonvirtual method available), this also satisfies the following two interop requirements:

  • Code re-use: we can combine nonvirtual methods from parent views (since modulo conflicts and overrides, a view which implements A and B has all of the nonvirtual methods of A and B available on it).

Things not addressed by default:

  • The ability to implement two interfaces A and B, and select a specific method to expose that exists in both. The workaround for this would be to re-implement it yourself to resolve the conflict?
  • The ability to not inherit something from a superinterface (assuming again that we take the "extension method" semantics".

Syntax

Putting "nonvirtual" on every method seems a little painful, though maybe not too bad. Alternatives:

  • We could say that view class methods (or whatever) are nonvirtual by default, and only require the keyword on normal class methods
  • We could use a segment in the class (`nonvirtual: )

If we use the extension method resolution semantics, we could consider using "extension" instead of "nonvirtual", since the semantics essentially become the same as defining an extension method with the class that gets imported with the type.

class A {
  extension int get x => 3;
}

Discussion

Pros:

  • I think this captures the core JS interop use cases (not sure about all of the code re-use cases) at least if we use the extension method resolution semantics
  • It helps with the surprising resolution behavior since we don't re-use the existing declaration syntax (you explicitly mark the declaration as nonvirtual), and since we add a general feature so that users will be more familiar with it.
  • It has a fairly clean semantic model
  • Decomposes the view feature into a few more orthogonal features (non-virtual methods + a wrapperless class)

Cons:

  • Adds more surface area to the language for users to understand
  • Adds a bunch of potentially surprising resolution behavior to all classes, rather than either restricting the weirdness to a corner of the language
  • Wider surface area to implement

cc @munificent @lrhn @eernstg @natebosch @jakemac53 @stereotype441 @mit-mit @johnniwinther @chloestefantsova @srawlins @mraleph

@leafpetersen leafpetersen added extension-types inline-classes Cf. language/accepted/future-releases/inline-classes/feature-specification.md labels Aug 10, 2022
@leafpetersen leafpetersen self-assigned this Aug 10, 2022
@leafpetersen
Copy link
Member Author

cc @kallentu

@Levi-Lesches
Copy link

How about treating non-virtual methods as static methods with a parameter for this? Sort of like Python's notion of self:

class A<T> { 
  final T data;
  const A(this.data);
  
  /// Simply returns [data].
  nonvirtual T getProperty() => data;
  // translated to: 
  static T getProperty<T>(A self) => self.data;
}

/// A specialized version of [A] that only works with [int]s. 
class B extends A<int> {
  const B(super.data);

  /// Returns the string form of [data], formatted with commas.
  ///
  /// This is normally an error since [String] is not a subtype of [int], but here 
  /// it is okay since [A.getProperty] belongs to [A]'s static interface.
  String getProperty => formatWithCommas(data);
}

void main() {
  print(A(1000).getProperty());  // "1000"
  // translated to
  print(A.getProperty(A(1000)));  // "1000"

  /// This is a regular virtual method call, unaffected by [A]
  print(B(1000).getProperty());  // "1,000"
}

With this interpretation, non-virtual methods are just instance methods that are only part of the class's static interface. And Dart users don't have to learn a new concept. So, for the questions above:

  1. Do we require compatible signatures in subclasses (we don't have to).

No, since the non-virtual method is not even inherited.

  1. Do classes "inherit" nonvirtual things that they don't override (the way that they inherit extension methods), or are nonvirtual methods only available on the interface that they are defined in?

No, since non-virtual methods are only in the static interface of the class that defines them.

...Don't ask how this will interact with #356.

@leafpetersen
Copy link
Member Author

How about treating non-virtual methods as static methods with a parameter for this?

This is the underlying semantic model of the proposal above. Whether we choose to "inherit" the dispatch in subclasses or not is a largely orthogonal choice, I think.

No, since non-virtual methods are only in the static interface of the class that defines them.

Yes, this is a choice we can make, though we then need some other mechanism to provide code re-use for the JS interop use case.

@leafpetersen
Copy link
Member Author

I see that @sigmundch had already filed an issue on this idea here.

@srujzs
Copy link

srujzs commented Aug 11, 2022

The ability to implement two interfaces A and B, and select a specific method to expose that exists in both. The workaround for this would be to re-implement it yourself to resolve the conflict?

The ability to not inherit something from a superinterface (assuming again that we take the "extension method" semantics".

Can we reuse the show/hide mechanisms from views to resolve these two issues? Presumably for the first case, we should get a static error asking the user to resolve either through hiding one method, showing the methods you only care about (and therefore purposefully do not result in collisions), or implementing their override. For example:

class A {
  nonvirtual String get val => 'A';
}

class B {
  nonvirtual String get val => 'B';
  nonvirtual String get otherVal => 'B';
}

class C implements A hide val, B {}

class D implements A, B show otherVal {} // implicitly hides B.val

class E implements A hide val, B hide val {
  // Multiple hides may be cumbersome, maybe an `@override` implicitly should hide any conflicts with that member?
  nonvirtual String get val => 'E';
}

void main() {
  A().val; // 'A'
  B().val; // 'B'
  C().val; // 'B'
  D().val; // 'A'
  E().val; // 'E'
}

@leafpetersen
Copy link
Member Author

Closing, we have a design.

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

No branches or pull requests

3 participants