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.go...@oracle.com
<mailto:brian.go...@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.