> De: "Brian Goetz" <brian.go...@oracle.com> > À: "amber-spec-experts" <amber-spec-experts@openjdk.java.net> > Envoyé: Mardi 27 Novembre 2018 23:20:54 > Objet: Sealed types
> Since we’re already discussing one of the consequences of sealed types, let’s > put the whole story on the table. These are my current thoughts, but there’s > room in the design space to wander a bit. > 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. > Note: It is probably desirable to ship this with records, though it could be > shipped before or after. > Declaration. We specify that a class is sealed by applying the sealed modifier > to a class, abstract class, interface, or record: > sealed interface Node { ... } > In this streamlined form, Node may be extended only by its nestmates. This may > be suitable for many situations, but not for all; in this case, the user may > specify an explicit permits list: > sealed interface Node > permits FooNode, BarNode { ... } > 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 same compilation unit. what inferring from the same compilation unit means ? - it works for anonymous class declared in the same compilation unit ? sealed interface Foo { static Foo create() { return new Foo() { ... }; // ok } } the compiler add Foo$1 in the permit list. - it works for functional interface for lambda in the same compilation unit ? sealed interface Fun { int apply(int value); static Fun identity() { return x -> x; // ok } } what exactly the permit list contains ?? (dynamic nestmate/sealed class to the rescue ?) And in another compilation unit Fun fun = x -> 2 * x; // rejected because Fun is sealed ? > 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. > (In the simplified form, the compiler computes the permits list by enumerating > the subtypes in the nest when the nest is declared, since they are in a single > compilation unit.) Permitted subtypes must belong to the same module (or, if > not in a module, the same package and protection domain.) > 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_SEALED > modifier (we could choose to overload ACC_FINAL for bit preservation), and a > Sealed 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, and a concrete subtype of a > sealed type is implicitly final. This can be reversed by explicitly modifying > the subtype with the non-sealed or non-final modifiers. > 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: > sealed interface ConstantDesc > permits String, Integer, Float, Long, Double, > ClassDesc, MethodTypeDesc, MethodHandleDesc, > DynamicConstantDesc { } > sealed interface ClassDesc extends ConstantDesc > permits PrimitiveClassDescImpl, ReferenceClassDescImpl { } > private class PrimitiveClassDescImpl implements ClassDesc { } > private class ReferenceClassDescImpl implements ClassDesc { } > sealed interface MethodTypeDesc extends ConstantDesc > permits MethodTypeDescImpl { } > sealed interface MethodHandleDesc extends ConstantDesc > permits DirectMethodHandleDesc, MethodHandleDescImpl { } > sealed interface DirectMethodHandleDesc extends MethodHandleDesc > permits DirectMethodHandleDescImpl > // designed for subclassing > non-sealed class DynamicConstantDesc extends ConstantDesc { ... } > Enforcement. Both the compiler and JVM should enforce sealing. > Note: It might be allowable for VM support to follow in a later version, > rather > than delaying the feature entirely. Does seems to be a good idea to divorce the two. It means you can create classes with ASM that will work for a version of the VM but not the next one. I understand the velocity problem (adding something in the VM is less easier than in the compiler) but by delaying the implementation in the VM, it means you make the code of the VM more complex because there is now 3 states: 1/ do not allow sealed flag on interface, 2/ allow sealed access flag on interface, 3/ verify sealed flag Given that the VM > Accessibility. Subtypes need not be as accessible as the sealed parent. In > this > case, clients are not going to 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.”) Yes ! I expect a public sealed interface with several package private implementations to be a common pattern. The implication is that you can not define (at least now) the implementations inside the sealed interface because all class members of an interface can only be public so you can not declare a package-private implementation. I don't know exactly why private class are not allowed in interface, it seems to be an overlook for me. > 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.) > Open question: 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. I think > it is worth considering relaxing this rule to permit for sealed classes, say: > allowing public auxilliary subtypes of the primary type, if the primary type > is > public and sealed. > > Syntactic alternative: Rather than inventing a new modifier (which then needs > a > negation modifier), we could generalize the meaning of final , such as: > final class C { } // truly final > final interface Node // explicit version > permits ANode, BNode { } > non-final class ANode implements Node { } // opt-out of transitivity > final interface Node // inferred version > permits <bikeshed> { } > This eliminates both sealed and non-sealed as modifiers. The downside is, of > course, that it will engender some “who moved my cheese” reactions. (We > currently have separate spellings for extends and implements ; some think this > was a mistake, and it surely complicated things when we got to generics, as we > created an asymmetry with <T extends U> . The move of retconning final avoids > replicating this mistake.) This is in line with the suggested retcon at the > classfile level as well. > Rémi