-
Notifications
You must be signed in to change notification settings - Fork 207
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
Write separate more detailed spec for sealed types. #2591
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -0,0 +1,198 @@ | ||||
# Sealed types | ||||
|
||||
Author: Bob Nystrom | ||||
|
||||
Status: In-progress | ||||
|
||||
Version 1.0 | ||||
|
||||
This proposal specifies *sealed types*, which is core capability needed for | ||||
[exhaustiveness checking][] of subtypes in [pattern matching][]. This proposal | ||||
is a subset of the [type modifiers][] proposal. (We may wish to do all or parts | ||||
of the rest of that proposal, but they aren't needed for pattern matching, so | ||||
this proposal separates them out.) For motivation, see the previously linked | ||||
documents. | ||||
|
||||
[exhaustiveness checking]: https://github.com/dart-lang/language/blob/master/working/0546-patterns/exhaustiveness.md | ||||
|
||||
[pattern matching]: https://github.com/dart-lang/language/blob/master/working/0546-patterns/patterns-feature-specification.md | ||||
|
||||
[type modifiers]: https://github.com/dart-lang/language/blob/master/working/type-modifiers/feature-specification.md | ||||
|
||||
## Introduction | ||||
|
||||
Marking a type `sealed` applies two restrictions: | ||||
|
||||
* If it's a class, the type itself can't be directly constructed. The class is | ||||
implicitly `abstract`. | ||||
|
||||
* All direct subtypes of the type must be defined in the same library. Any | ||||
types that directly implement, extend, or mixin the sealed type must be | ||||
defined in the library where the sealed type is defined. | ||||
|
||||
In return for those restrictions, sealed provides two useful properties for | ||||
exhaustiveness checking: | ||||
|
||||
* All of the direct subtypes of the sealed type can be easily found and | ||||
enumerated. | ||||
|
||||
* Any concrete instance of the sealed type must also be an instance of at | ||||
least one of the known direct subtypes. In other words, if you match on a | ||||
value of the sealed type and you have cases for all of the direct subtypes, | ||||
the compiler knows those cases are exhaustive. | ||||
|
||||
### Open subtypes | ||||
|
||||
Note that it is *not* necessary for the subtypes of a sealed type to themselves | ||||
be sealed or closed to subclassing or implementing. Given: | ||||
|
||||
```dart | ||||
sealed class Either {} | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sold on There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is consistent with how I was surprised too, but my informal understanding of
If you want that, you can always mark the subtypes Fundamentally, the way I've approached this design is, "What is the simplest set of restrictions that we need for exhaustiveness checking to work." As far as pattern matching cares, if you have a sealed type |
||||
|
||||
class Left extends Either {} | ||||
class Right extends Either {} | ||||
``` | ||||
|
||||
Then this switch is exhaustive: | ||||
|
||||
```dart | ||||
test(Either either) { | ||||
switch (either) { | ||||
case Left(): print('Left'); | ||||
case Right(): print('Right'); | ||||
} | ||||
} | ||||
``` | ||||
|
||||
And this is still true even if some unrelated or unknown library contains: | ||||
|
||||
```dart | ||||
class LeftOut extends Left {} | ||||
``` | ||||
|
||||
Or even: | ||||
|
||||
```dart | ||||
class Ambidextrous implements Left, Right {} | ||||
``` | ||||
|
||||
The only property we need for exhaustiveness is that *all instances of the | ||||
sealed type must also be an instance of a direct subtype.* More precisely, any | ||||
instance of the sealed supertype must have at least one of the direct subtypes | ||||
in its superinterface graph. | ||||
|
||||
### Sealed subtypes | ||||
|
||||
At the same time, it can be useful to seal not just a supertype but one or more | ||||
of its subtypes. Doing so lets you define a sealed *hierarchy* where matching | ||||
various subtypes will exhaustively cover various branches of the hierarchy. For | ||||
example: | ||||
|
||||
```dart | ||||
// UnitedKingdom --+-- NorthernIreland | ||||
// | | ||||
// +-- GreatBritain --+-- England | ||||
// | | ||||
// +-- Scotland | ||||
// | | ||||
// +-- Wales | ||||
sealed class UnitedKingdom {} | ||||
class NorthernIreland extends UnitedKingdom {} | ||||
sealed class GreatBritain extends UnitedKingdom {} | ||||
class England extends GreatBritain {} | ||||
class Scotland extends GreatBritain {} | ||||
class Wales extends GreatBritain {} | ||||
``` | ||||
|
||||
By marking not just `UnitedKingdom` `sealed`, but also `GreatBritain` means that | ||||
all of these switches are exhaustive: | ||||
|
||||
```dart | ||||
test1(UnitedKingdom uk) { | ||||
switch (uk) { | ||||
case NorthernIreland(): print('Northern Ireland'); | ||||
case GreatBritain(): print('Great Britain'); | ||||
} | ||||
} | ||||
|
||||
test2(UnitedKingdom uk) { | ||||
switch (uk) { | ||||
case NorthernIreland(): print('Northern Ireland'); | ||||
case England(): print('England'); | ||||
case Scotland(): print('Scotland'); | ||||
case Wales(): print('Wales'); | ||||
} | ||||
} | ||||
|
||||
test3(GreatBritain britain) { | ||||
switch (britain) { | ||||
case England(): print('England'); | ||||
case Scotland(): print('Scotland'); | ||||
case Wales(): print('Wales'); | ||||
} | ||||
} | ||||
``` | ||||
|
||||
Note that the above examples are all exhaustive regardless of whether | ||||
`NorthernIreland`, `England`, `Scotland`, and `Wales` are marked `sealed`. | ||||
|
||||
In short, `sealed` is mostly a property that affects how you can use the | ||||
*supertype* and does not apply any restrictions to the direct subtypes of the | ||||
sealed type, except that they must be defined in the same library. | ||||
|
||||
## Syntax | ||||
|
||||
A class or mixin declaration may be preceded with the built-in identifier | ||||
`sealed`: | ||||
|
||||
``` | ||||
classDeclaration ::= | ||||
( 'abstract' | 'sealed' )? 'class' identifier typeParameters? | ||||
superclass? interfaces? | ||||
'{' (metadata classMemberDeclaration)* '}' | ||||
| ( 'abstract' | 'sealed' )? 'class' mixinApplicationClass | ||||
|
||||
mixinDeclaration ::= 'sealed'? 'mixin' identifier typeParameters? | ||||
('on' typeNotVoidList)? interfaces? | ||||
'{' (metadata classMemberDeclaration)* '}' | ||||
``` | ||||
|
||||
*Note that the grammar disallows `sealed` on a class marked `abstract`. All | ||||
sealed types are abstract, so it's redundant to allow both modifiers.* | ||||
|
||||
**Breaking change:** Treating `sealed` as a built-in identifier means that | ||||
existing code that uses `sealed` as the name of a type will no longer compile. | ||||
Since almost all types have capitalized names in Dart, this is unlikely to be | ||||
break much code. | ||||
munificent marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
|
||||
### Static semantics | ||||
|
||||
It is a compile-time error to extend, implement, or mix in a type marked | ||||
`sealed` outside of the library where the sealed type is defined. *It is fine, | ||||
however to subtype a sealed type from another part file or [augmentation | ||||
library][] within the same library.* | ||||
|
||||
[augmentation library]: https://github.com/dart-lang/language/blob/master/working/augmentation-libraries/feature-specification.md | ||||
|
||||
A typedef can't be used to subvert this restriction. If a typedef refers to a | ||||
sealed type, it is also a compile-time error to extend, implement or mix in that | ||||
typedef outside of the library where the sealed the typedef refers to is | ||||
defined. *Note that the library where the _typedef_ is defined does not come | ||||
into play.* | ||||
|
||||
A class marked `sealed` is implicitly an *abstract class* with all of the | ||||
existing restrictions and capabilities that implies. *It may contain abstract | ||||
member declarations, it is a compile-time error to directly invoke its | ||||
constructors, etc.* | ||||
|
||||
### Runtime semantics | ||||
|
||||
There are no runtime semantics. | ||||
|
||||
### Core library | ||||
|
||||
The "dart:core" types `bool`, `double`, `int`, `Null`, `num`, and `String` are | ||||
all marked `sealed`. *These types have always behaved like sealed types by | ||||
relying on special case restrictions in the language specification. That | ||||
existing behavior can now be expressed in terms of this general-purpose | ||||
feature.* | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is precisely the reason using the word I don't want people to start using exhaustive-sealed instead of really-sealed, just because it's the only thing they have. For one thing, it doesn't work, because the class becomes abstract. I guess it can work, but then you need to introduce a private non-abstract subclass, just to get the sealing. We generally pretend that An alternative is to actually use Then you can seal all your types, abstract or not, in order to prevent implementations from outside the library. So, I worry about using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I believe these types do behave like types marked It is true that marking a class There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure about There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'm fine with whatever we want to seal, though adding to this list might mean breaking changes, which I'd like to avoid in the interests of expedience. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, it's the TypedData classes. But the point is they're already sealed. dart-lang/sdk#45115 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Anyway,
This These classes are a different kind of sealed, the transitive one, not just the exhaustiveness one. For typed-data, there really are subclasses in the library, because all the constructors are factory ones. We'd still use Misusing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
As long as those breaking changes are gated by the language version it shouldn't be a deal breaker. We should definitely consider whether there are any other classes worth sealing if we think there is an easy enough migration path. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For the purposes of making exhaustiveness checking work, this distinction doesn't matter for types like (We could even potentially spec There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
language/specification/dartLangSpec.tex Line 2500 in 04e34f8
The spec says a lot of things, some of them even true. :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Some truths have been updated, and may even be landed at some point. ;-) In particular, https://github.com/dart-lang/language/pull/2605/files#diff-0e2f05538272f3fb682a361d7738d860125088af3ac0044e5472cac965cfdb61R24431 has: The \code{Null} class declares exactly the same members
with the same signatures as the class \code{Object}. and the text weasels around the question about the superclass, because I'd prefer that we introduce a class |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the reasoning for this restriction?
I'm thinking that we can allow the superclass to be instantiated. Then we need to include that class itself in the set of types to check to achieve exhaustiveness. You can still write
abstract
to avoid it.Disallowing it should have a reason. I'm sure there are plenty of good reason, I'm just curious which ones we've used to reach the conclusion :)
I can come up with:
abstract
on the superclass.abstract
on the superclass, then their switches are not exhaustive.abstract
on superclass to decide whether it needs to be part of exhaustiveness checking, we are adding implicit meaning on top of just beingabstract
. That really should be explicit opt-in.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's necessary to make sealed types useful. If you can have direct instances of the supertype, then in pattern matching, you have to explicitly check the supertype in addition to its subtypes. But if you have to check the supertype anyway... then you derive absolutely no benefit from marking it
sealed
in the first place. The whole point of sealing is to know that if you've handled all the subtypes you are done. Direct instances of the supertype break that.