Sealed types
Brian Goetz
brian.goetz at oracle.com
Fri Dec 7 18:57:07 UTC 2018
And if we're trying to save on new keywords:
permits this class
:)
On 12/7/2018 1:27 PM, Guy Steele wrote:
> How about “permits class”?
>
> (if you don’t like that, then all I have left to offer are “permits
> catch” and “permits goto”. :-)
>
>> On Dec 7, 2018, at 12:50 PM, Brian Goetz <brian.goetz at oracle.com
>> <mailto:brian.goetz at oracle.com>> wrote:
>>
>> The most obvious remaining question appears to be: how do we spell
>> "permits <nest-members>".
>>
>> We could say "permits this", which has the advantage of being a
>> keyword, but doesn't really mean what it says.
>>
>> We could say "permits local", though "local" usually means local to a
>> method.
>>
>> We could say "permits nest" or "permits nested", though if we decide
>> to allow public auxilliary subclasses in this case, then they are not
>> likely to be nestmates, and surely not nested.
>>
>> We could allow the permits clause to be omitted, and be interpreted
>> as "permits nest", but this would much more severely change the
>> meaning of final -- people have an idea what final means, and this
>> surely wouldn't be it.
>>
>> Open to other ideas...
>>
>> (This is one of the costs of retconning final; if we used "sealed",
>> we wouldn't need a permits clause at all. Overall its probably still
>> a good tradeoff, but we do have to paint this shed acceptably.)
>>
>>
>>
>> On 12/7/2018 11:38 AM, Brian Goetz wrote:
>>>
>>> I’ve updated the document on sealing to reflect the discussion so far.
>>>
>>>
>>> Sealed Classes
>>>
>>> *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
>>> |final| modifier to a class, abstract class, interface, or record,
>>> and specifying a |permits| list:
>>>
>>> |final interface Node permits A, B, C { ... } |
>>>
>>> In this explicit form, |Node| may be extended only by the types
>>> enumerated in the |permits| list (which must further be members of
>>> the same package or module.)
>>>
>>> In many situations, this may be overly explicit; if all the subtypes
>>> are declared in the same compilation unit, we may wish to permit a
>>> streamlined form of the |permits| clause, that means “may be
>>> extended by classes in the same compilation unit.”
>>>
>>> |final interface Node permits __nestmates { ... } |
>>>
>>> (As usual, pseudo-keywords beginning with |__| are placeholders to
>>> illustrate the overall shape of the syntax.)
>>>
>>> We can think of the simpler form as merely inferring the full
>>> |permits| clause from information already present in the source file.
>>>
>>> Anonymous subclasses (and lambdas) of a sealed type are prohibited.
>>>
>>> *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.)
>>>
>>> /Note:/ It is superficially tempting to say |permits package| or
>>> |permits module| as a shorthand, which would allow 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, because packages and modules are not always
>>> co-compiled. 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| accessibility bit, and a |PermittedSubtypes| attribute
>>> which contains a list of permitted subtypes (similar in structure to
>>> the nestmate attributes.) Classes with |ACC_FINAL| but without
>>> |PermittedSubtypes| behave like traditional final classes.
>>>
>>> *Sealing is inherited.* Unless otherwise specified, abstract
>>> subtypes of sealed types are implicitly sealed, and concrete
>>> subtypes are implicitly final. This can be reversed by explicitly
>>> modifying the subtype with |non-final|.
>>>
>>> Unsealing a subtype in a hierarchy doesn’t undermine all the
>>> benefits of 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 inheritance,
>>> requiring sealing to be opted into at all levels. This is widely
>>> believed to be a source of bugs; it is relatively rare that one
>>> actually wants a subtype of a sealed type to not be sealed, and in
>>> those cases, is best to be explicit. Not inheriting would be a
>>> simpler rule, 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:
>>>
>>> |final interface ConstantDesc permits String, Integer, Float, Long,
>>> Double, ClassDesc, MethodTypeDesc, MethodHandleDesc,
>>> DynamicConstantDesc { } final interface ClassDesc extends
>>> ConstantDesc permits PrimitiveClassDescImpl, ReferenceClassDescImpl
>>> { } private class PrimitiveClassDescImpl implements ClassDesc { }
>>> private class ReferenceClassDescImpl implements ClassDesc { } final
>>> interface MethodTypeDesc extends ConstantDesc permits
>>> MethodTypeDescImpl { } final interface MethodHandleDesc extends
>>> ConstantDesc permits DirectMethodHandleDesc, MethodHandleDescImpl {
>>> } 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, as
>>> they both enforce finality today (though from a project-management
>>> standpoint, it might be allowable for VM support to follow in a
>>> later version, rather than delaying the feature entirely.)
>>>
>>> *Accessibility.* Subtypes need not be as accessible as the sealed
>>> parent. In this case, some 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.”)
>>>
>>> *Javadoc.* The list of permitted subtypes should be 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, or including an annotation that there exist others
>>> that are not listed.)
>>>
>>> /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.
>>>
>>>
>>
>
More information about the amber-spec-observers
mailing list