I'm still not 100% sure that mixing the exhaustiveness and the closeness is a good idea, again because - you may want closeness of non user named types - you may want exhaustiveness not only types (on values by example) but it makes the feature simple, so let's go that way.
Allowing public auxillary subtype of a primary sealed type is the sweet spot for me, better than trying to introduce either a nesting which is not exactly nesting or a rule than only works for pattern matching. I don't understand how "semi-final" can be a good keyword, the name is too vague. Given that the proposal introduce the notion of sealed types, "sealed" is a better keyword. For un-sealing a subtype, "unsealed" seems to be a good keyword. Rémi > De: "Brian Goetz" <brian.go...@oracle.com> > À: "amber-spec-experts" <amber-spec-experts@openjdk.java.net> > Envoyé: Mercredi 9 Janvier 2019 19:44:12 > Objet: Sealed types -- updated proposal > Here's an update on the sealed type proposal based on recent discussions. > Definition. A sealed type is one for which subclassing is restricted according > to guidance specified with the type’s declaration; finality can be considered > a > degenerate form of sealing, where no subclasses at all are permitted. Sealed > types are a sensible means of modeling algebraic sum types in a nominal type > hierarchy; they go nicely with records ( algebraic product types ), though are > also useful on their own. > Sealing serves two distinct purposes. The first, and more obvious, is that it > restricts who can be a subtype. This is largely a declaration-site concern, > where an API owner wants to defend the integrity of their API. The other is > that it potentially enables exhaustiveness analysis at the use site when > switching over sealed types (and possibly other features.) This is less > obvious, and the benefit is contingent on some other things, but is valuable > as > it enables better compile-time type checking. > Declaration. We specify that a class is sealed by applying the semi-final > modifier to a class, abstract class, or interface: > semi-final interface Node { ... } > In this streamlined form, Node may be extended only by named classes declared > in > the same nest. This may be suitable for many situations, but not for all; in > this case, the user may specify an explicit permits list: > semi-final interface Node > permits FooNode, BarNode { ... } > Note: permits here is a contextual keyword. > The two forms may not be combined; if there is a permits list, it must list > all > the permitted subtypes. We can think of the simple form as merely inferring > the > permits clause from information in the nest. > Exhaustiveness. One of the benefits of sealing is that the compiler can > enumerate the permitted subtypes of a sealed type; this in turn lets us > perform > exhaustiveness analysis when switching over patterns involving sealed types. > Permitted subtypes must belong to the same module (or, if not in a module, the > same package.) > Note: It is superficially tempting to have a relaxed but less explicit form, > say > which allows for a type to be extended by package-mates or module-mates > without > listing them all. However, this would undermine the compiler’s ability to > reason about exhaustiveness. This would achieve the desired subclassing > restrictions, but not the desired ability to reason about exhaustiveness. > Classfile. In the classfile, a sealed type is identified with an ACC_FINAL > modifier, and a PermittedSubtypes attribute which contains a list of permitted > subtypes (similar in structure to the nestmate attributes.) > Transitivity. Sealing is transitive; unless otherwise specified, an abstract > subtype of a sealed type is implicitly sealed (permits list to be inferred), > and a concrete subtype of a sealed type is implicitly final. This can be > reversed by explicitly modifying the subtype with the non-final modifier. > Unsealing a subtype in a hierarchy doesn’t undermine the sealing, because the > (possibly inferred) set of explicitly permitted subtypes still constitutes a > total covering. However, users who know about unsealed subtypes can use this > information to their benefit (much like we do with exceptions today; you can > catch FileNotFoundException separately from IOException if you want, but don’t > have to.) > Note: Scala made the opposite choice with respect to transitivity, requiring > sealing to be opted into at all levels. This is widely believed to be a source > of bugs; it is rare that one actually wants a subtype of a sealed type to not > be sealed. I suspect the reasoning in Scala was, at least partially, the > desire > to not make up a new keyword for “not sealed”. This is understandable, but I’d > rather not add to the list of “things for which Java got the defaults wrong.” > An example of where explicit unsealing (and private subtypes) is useful can be > found in the JEP-334 API: > semi-final interface ConstantDesc > permits String, Integer, Float, Long, Double, > ClassDesc, MethodTypeDesc, MethodHandleDesc, > DynamicConstantDesc { } > semi-final interface ClassDesc extends ConstantDesc > permits PrimitiveClassDescImpl, ReferenceClassDescImpl { } > private class PrimitiveClassDescImpl implements ClassDesc { } > private class ReferenceClassDescImpl implements ClassDesc { } > semi-final interface MethodTypeDesc extends ConstantDesc > permits MethodTypeDescImpl { } > semi-final interface MethodHandleDesc extends ConstantDesc > permits DirectMethodHandleDesc, MethodHandleDescImpl { } > semi-final interface DirectMethodHandleDesc extends MethodHandleDesc > permits DirectMethodHandleDescImpl > // designed for subclassing > non-final class DynamicConstantDesc extends ConstantDesc { ... } > Enforcement. Both the compiler and JVM should enforce sealing. > Accessibility. Subtypes need not be as accessible as the sealed parent. In > this > case, not all clients will get the chance to exhaustively switch over them; > they’ll have to make these switches exhaustive with a default clause or other > total pattern. When compiling a switch over such a sealed type, the compiler > can provide a useful error message (“I know this is a sealed type, but I can’t > provide full exhaustiveness checking here because you can’t see all the > subtypes, so you still need a default.”) > Javadoc. The list of permitted subtypes should probably be considered part of > the spec, and incorporated into the Javadoc. Note that this is not exactly the > same as the current “All implementing classes” list that Javadoc currently > includes, so a list like “All permitted subtypes” might be added (possibly > with > some indication if the subtype is less accessible than the parent.) > Auxilliary subtypes. With the advent of records, which allow us to define > classes in a single line, the “one class per file” rule starts to seem both a > little silly, and constrain the user’s ability to put related definitions > together (which may be more readable) while exporting a flat namespace in the > public API. > One way to do get there would be to relax the “no public auxilliary classes” > rule to permit for sealed classes, say: allowing public auxilliary subtypes of > the primary type, if the primary type is public and sealed. > Another would be to borrow a trick from enums; for a sealed type with nested > subtypes, when you import the sealed type, you implicitly import the nested > subtypes too. That way you could declare: > semi-final interface Node { > class A implements Node { } > class B implements Node { } > } > but clients could import Node and then refer to A and B directly: > switch (node) { > case A(): ... > case B(): ... > } > We do something similar for enum constants today. >