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

Type capability modifiers #2242

Closed
munificent opened this issue May 13, 2022 · 10 comments
Closed

Type capability modifiers #2242

munificent opened this issue May 13, 2022 · 10 comments
Assignees
Labels
class-modifiers Issues related to "base", "final", "interface", and "mixin" modifiers on classes and mixins. feature Proposed language feature that solves one or more problems

Comments

@munificent
Copy link
Member

munificent commented May 13, 2022

EDITED: After quite a bit of discussion, a proposal based off of the description below (but with some different choices in syntax) has been accepted here.

Users have asked for the ability to prevent a class from being extended (#987) and/or implemented (#704). For exhaustiveness checks in pattern matching, we also need the ability to define a type with a known closed set of subtypes.

The Type Modifiers proposal addresses those. It's an alternative to the earlier Packaged Libraries proposal. The very brief summary is:

  • Remove the ability to use a class as a mixin (Do not allow mixing in a class, with migration path #1643).
  • Types default to fully permissive as they are today.
  • Use closed on a class to disable extending it.
  • Use base on a class or mixin to disables its implicit interface.
  • Use a switch modifier on a class or mixin to defines the root of a sealed type family for exhaustiveness checking. It also implies abstract.

This issue is to discuss the proposal. Feel free to file other more specific issues too.

@munificent munificent added feature Proposed language feature that solves one or more problems patterns Issues related to pattern matching. modules labels May 13, 2022
@leafpetersen leafpetersen added this to Being discussed in Language funnel Jun 15, 2022
@albertodev01
Copy link

I really, deeply like this proposal! The only I would change is:

Use closed on a class to disable extending it.

I'd prefer something like final or sealed which is more familiar in the OOP world (see like Java, C#, Delphi, Kotlin etc...)

Use a switch modifier on a class or mixin to defines the root of a sealed type family for exhaustiveness checking. It also implies abstract.

I have also read some documentation you've linked but I'm not sure I've understood the meaning of this keyword and the context, Could you please share an example?

Thank you

@lrhn
Copy link
Member

lrhn commented Jun 21, 2022

A switch class is a requirement to not declare direct subclasses outside of the current module (library/package/whichever granularity is chosen)
It's also an implicit promise to not declare further direct subclasses in the future.

With those promises, it's possible for a "type switch", as the one tha is part of the patterns language feature proposal, to know that a the switch is exhaustive.

Say the switch class Primary color has the three direct subclasses Red, Green and Blue declared in the same module. Then code somewhere else can do:

int colorValue = switch (somePrimaryColor) {
  case Red r => numberForRed(r);
  case Blue b => numberForBlue(b);
  case Green g => numberForGreen(g);
}

and know that there doesn't need to be a default case because the three cases are exhaustive.
There cannot be any further subclasses classes declared anywhere else.

If you later add a new direct subclass in the same module, you break this existing code. That's why it's also an implicit promise not to do that (unless you increase the major version number of your package).

@munificent
Copy link
Member Author

I'd prefer something like final or sealed which is more familiar in the OOP world (see like Java, C#, Delphi, Kotlin etc...)

Yeah, I considered that, as we could use one of those. But I specifically chose a new keyword because those keywords mean something different in some other languages:

  • If we used final, a Java user would likely be surprised that a class marked final can still be implemented as an interface and can even be extended in the same library.
  • If we used sealed a Java or Scala user would be surprised that a class marked sealed does not give you exhaustiveness checks.

@TzviPM
Copy link

TzviPM commented Jul 5, 2022

@munificent is there a reason that someone might want closed but not base? Could we use final instead of closed and make final also imply that the implicit interface is disabled?

The idea of something being closed for extension outside of a library but extension being allowed internally seems interesting. It's similar to private members that are accessible within the library as well, I suppose. In that case, the same issue of a Java user would likely be surprised that a class marked final can still be implemented as an interface and can even be extended in the same library would apply there, no?

Should there be a concept of imposing restrictions within a library itself? If so, perhaps that should be handled in a separate proposal, as it would also affect private members.

@lrhn
Copy link
Member

lrhn commented Jul 6, 2022

Dart generally do not try to protect code against other code in the same library.

Making something inaccessible or prohibited to the same library is so soft a boundary that it's almost non-existing, because whoever can write code to do the prohibited thing, can also remove the prohibition, or code around it. The only thing that can actually stop editors of the same library from doing things you don't want them to, is social contract, not the compiler.

That's why it's not a goal to prevent you from breaking code in the same library.
The library is the unit of editing (or, in some cases, the package is the unit of editing) meaning that if you break something by editing the code, you can also edit the broken code and fix that too.

As for keywords - we'll need to new ones, and reusing old ones, to cover all the combinations here.
If we want to cover all combinations.

Other languages use final for preventing subclassing, but those languages don't have implicit interfaces like Dart. Dart could use final for classes with no subtyping, but we then still need names for "allows subclassing but not implementing" (enforced shared implementation) and "allows implementing but not subclassing" (private implementation of public interface).
If sealed sounds stronger than final (can be argued), maybe it should be used for the stronger restriction.

Other languages use sealed class or enum class for switch-able types. I actually like switch class.

@gochev
Copy link

gochev commented Jul 13, 2022

I would add that Dart is very similar to Java and C#, want or don't like this , but it is a fact. So instead of adding new keywords it is always better to use existing ones.. like final / sealed.
Also it is best to have as less keywords as possible, otherwise the language becomes like Kotlin and no one actually likes it.

Still, I strongly believe that all public by default and so on helps Dart and we already have final as a keyword and _ prefix. So I would stick with this two...

Finally base is just a bad name... you eighter use super or base keywords and to have both doing different things just creates a fuzz and mess.

@munificent
Copy link
Member Author

@munificent is there a reason that someone might want closed but not base?

Yes, I think so. This is essentially what you get in other languages by exposing a public interface and a private "impl" class that implements it. It says: "You can implement this protocol yourself from scratch, or you can use this default implementation, but you can't reuse pieces of the code in the default implementation." That combination makes it easier to evolve the implementation of the class since you know there aren't any users subclassing it and overriding random methods in unexpected ways.

Or, another use case is "You can mock this class for tests, but if you're using the real thing you have to use exactly this real thing."

Could we use final instead of closed and make final also imply that the implicit interface is disabled?

I did consider that, but I think it's worth being able to vary the two concepts independently. I think each combination has meaningful use cases:

  • Open and implementable: You can define new subtypes and you are free to reuse as much of the base class implementation as you want or not at all. This is the default in Dart.
  • Closed and implementable: See above.
  • Open and base: You can use or extend this class, but you can't discard its implementation. Every place that accepts a value of this type knows it will get an instance that does concretely inherit this class's code. This can be important if you are, for example, calling private methods on the object and want to know they're there. This is the default for most classes in other languages.
  • Closed and base. This is a concrete class that you can use directly but is not open for extension in any way. It lets an API know for certain exactly what objects it is working with and their implementation. This is the default in Swift, Scala, and Kotlin if I recall right.

I admit that "base" isn't ideal. It's the best I've been able to come up with.

@mikaelj
Copy link

mikaelj commented Dec 26, 2022

switch class is very syntax-oriented - wouldn't it be better to describe it in terms of what it actually does?

And this entire type capability modifiers suggestion -- isn't it sort-of superfluous if we'd have ADTs like in Haskell?

@mit-mit mit-mit moved this from Being spec'ed to Being implemented in Language funnel Jan 23, 2023
@munificent munificent added class-modifiers Issues related to "base", "final", "interface", and "mixin" modifiers on classes and mixins. and removed patterns Issues related to pattern matching. modules labels Mar 28, 2023
@mit-mit
Copy link
Member

mit-mit commented Apr 19, 2023

Closing; this is available in the beta channel, and is scheduled for our next stable release.

@mit-mit
Copy link
Member

mit-mit commented Nov 6, 2023

Documentation for this feature is now available at https://dart.dev/language/class-modifiers-for-apis

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
class-modifiers Issues related to "base", "final", "interface", and "mixin" modifiers on classes and mixins. feature Proposed language feature that solves one or more problems
Projects
Status: Done
Development

No branches or pull requests

7 participants