RFR: JEP 360: Sealed Types (Preview)
Alex Buckley
alex.buckley at oracle.com
Mon Apr 13 18:22:31 UTC 2020
// amber-dev only; don't see the need to cross-post to compiler-dev
On 4/13/2020 9:03 AM, Vicente Romero wrote:
> Before we were planning to infer finality, sealness or non-sealness
> in the subtypes. We steered away from that direction in favor of
> explicit declaration at the subtype. I would like to ask for another
> review of the current version of the JEP that reflects these
> changes.
I think the message of this important JEP is about halfway there. I'm
going to provide comments in email rather than actioning them directly,
because I want to spur discussion on "why" not "what" or "how".
- Goals and Non-Goals are meant to precede Motivation. This helps to
ensure that the Goals are not simply a restatement of the problem (from
the Motivation) or the solution (from the Description). Currently, the
Goals (well, Goal) is more or less the solution. I think the first goal
should be "Allow the author of a type to control which code is
responsible for implementing the type." I'm borrowing the i-word from
the introduction of JLS 6.6.2, which is superb despite its subsections
being mind-bending. I'm also previewing the theme in the Motivation
about helping implementors, not users (and thus the idea of
implementation code being different than user code). BTW, you'll see
later that I generally prefer "class or interface" to "type", but it
would be confusing to say "class or interface" and then only say
"implementing" (would look like we forgot "extending").
- The JEP says "Access control allows the library author to constrain
which packages can access and therefore implement the library, but
***cannot distinguish an implementor from a user***." First, I can't
tell what the library author is meant to be doing -- please show me.
Second, the starred text is not the fully story: the traditional way to
restrict an implementation hierarchy is for the restrictive superclass
to be package-private (AbstractStringBuilder) while the implementations
are co-located in the same package and final and public (StringBuilder +
StringBuffer). Please spell out and slam this scheme in the Motivation
as being hard to maintain -- partly because it fails to enumerate the
implementations, but mostly because it inverts responsibility: the
_implementations_ are on the hook to lock down the hierarchy, not the
superclass who desires the restriction. Since the implementations are
necessarily public, if any one of them gets it wrong (forgets to be
`final`) then all the others who got it right are undermined. There is
no _documented_ exhaustiveness (no list of subtypes) and no _actual_
exhaustiveness (anyone can now extend the public non-final implementation).
- The above suggests a second goal: "Provide a more declarative way than
accessibility modifiers to restrict the use of a superclass." -- the
r-word from the goal conveniently appears again in the Motivation.
Eventually, sealing will be the mechanism by which the
restrictions-on-use are realized.
- New non-goal: "Do not change `final` in any way."
- Description: "A sealed type is one for which subtyping is restricted
according to guidance specified with the type's declaration." is not
wrong, but it's quite abstract for something that will be copy-pasted
into 1000 articles and tutorials. The focus on "type" rather than "class
and interface" is also rather technical. Suggest: "A _sealed_ class or
interface can be extended or implemented only by those classes and
interfaces permitted to do so."
- This: "We specify that a class is sealed by applying the sealed
modifier to a class or interface, with an optional permits clause:" will
trip up readers because it mentions a class, then a class or interface,
then shows an example with an interface; and moreover `permits` is
optional but it's shown explicitly. Too much going on. Start simple: "A
class is sealed by applying the `sealed` modifier to THE class. Then,
after any `extends` clause to specify the superclass, the subclasses are
specified with a `permits` clause. For example:" ... `package
com.example.geometry; sealed class Shape permits Circle, Rectangle,
Square {...}` (I mention `extends` because `permits` does not exist in
a vacuum; it's in the same syntactic neighborhood as `extends`, so let's
draw out that the class inhabits a particular semantic neighborhood
within the overall class hierarchy: immediate super and immediate subs.)
- "The explicit `permits` clause means that `Shape` may be extended only
by the classes enumerated by the clause. These classes must be members
of the same module or, if in the unnamed module, the same package. For
example, assuming that `com.example.**` packages are in the same module:
`package com.example.geometry; sealed class Shape permits
com.example.polar.Circle, com.example.quad.Rectangle,
com.example.quad.Square {}` (Always promote named packages. Avoid
building examples around unnamed packages and the unnamed module.)
- Now have "Similarly, an interface is sealed by applying the `sealed`
modifier to the interface. After any `implements` clause to specify
superinterfaces, the implementations are specified with a `permits`
clause. For example:" Node / PlusNode / SubNode / MulNode / DivNode example
- "In many situations ..." -- please don't handwave. This is our chance
to have 1000 articles and tutorials recommend the right thing. "When
implementations are small in size and number, it may be convenient to
declare them in the same source file [NO-ONE KNOWS WHAT A COMPILATION
UNIT IS] as the superclass or superinterface. In this case, the
`permits` clause may be omitted, and the compiler will infer the
permitted subtypes from the implementations declared in the same source
file."
- Now a paragraph showing that `sealed` and `final` work together: say
that the permitted Circle, Rectangle, and Square types may be `final` to
prevent the Shape hierarchy from being inadvertently extended. Say a
class cannot be both `sealed` (implying subclasses) and `final`
(implying no subclasses) at the same time. Don't worry about interfaces.
- Now speak to something that has been buried, unacceptably in my view,
in a technical list of errors: "It is an error if a class has a sealed
direct superclass or superinterface and the class is not an enum type or
the class is not explicitly declared sealed, final or non-sealed." Say
"On the other hand, the hierarchy may extend deeper than these
explicitly permitted subtypes. To ensure that the hierarchy is not
accidentally opened for extension, a permitted subtype that is not
declared `final` must either be declared `sealed` itself, or be declared
`non-sealed`. If it is `sealed`, then it may have its own `permits`
clause. If it is `non-sealed`, then it may be subclassed without
restriction. For example:" ...
If there is one takeaway from this mail, it's that a list of
compile-time errors in a JEP is a bug, not a feature. A JEP should have
explained the Java language's goals and mechanisms in such a way that
the reader already knows what's allowed.
Alex
More information about the amber-dev
mailing list