Sealed types
Kevin Bourrillion
kevinb at google.com
Fri Nov 30 01:12:11 UTC 2018
On Tue, Nov 27, 2018 at 2:21 PM Brian Goetz <brian.goetz at oracle.com> wrote:
*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.
>
Clearly enums would be implicitly sealed (a redundant `sealed` keyword
presumably being ignored), and I assume we'll make sure they appear/behave
exactly the same as other sealed classes.
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.
>
Today, only non-interface classes can get something very close to that
protection, and I think that is the only reason Guava's `ImmutableList` is
a class instead of an interface. Which has been a bummer, since that
`class` keyword has given many users the wrong impression of how it's meant
to be used.
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.
>
Another example application: compiler (or static analysis tool) can
recognize bad casting conversions from/to the sealed type - maybe even
*all* bad
casting conversions when no subtype "unseals"? This is minor, but of
course, anything that makes more code recognizable as erroneous is also
going to help auto-complete work better and so on.
*Note:* It is probably desirable to ship this with records, though it could
> be shipped before or after.
>
Interesting: I would have expected you to say exactly that but for
pattern-matching instead of records.
> *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.
>
I suspect we ought to *recommend* use of `permits` as a kindness to users
who aren't always looking at javadoc, so they can actually see what to
switch over. Maybe requiring `permits` always is too much though (it also
precludes anonymous subtypes, but then again those are of limited value
anyway, aren't they?).
So back to the current state. I'm not 100% following why `permits`
shouldn't just be additive when present. That avoids the "cliff" and we do
generally trust source files to not misuse *themselves*.
Or, if not additive, but we end up reusing the `final` keyword in the way
shown at the bottom of this email, then we could at least allow `permits
*<bikeshed>*, TypeA, TypeB` which is maybe nearly as good.
*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.)
>
As before: I would expect any final class or enum to have ACC_SEALED set,
correct?
*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.
>
(FWIW, I found the idea of an unsealed subtype of a sealed supertype
massively confusing for a good while until I finally figured out why
there's nothing wrong with it.)
Do you mean the last statement above as "respectively" (non-sealed can
counteract implicit sealed of abstract; non-final can counteract implicit
final of concrete), or does `non-sealed` also work to counteract the
implicit final of a concrete class? For that matter shouldn't `sealed`
implicit undo the implicit `final` of a concrete class? I admit to still
being fairly confused right now.
(Syntax: I assume that the syntax is still malleable and not what needs to
be debated here and now. Nevertheless, I don't want to miss my chance to
object to the hyphenation for the record. These won't be seen as two new
keywords, but as a modifier-modifier, and users will not understand when
`non-` works and when it doesn't. I think `unsealed` and `nonfinal`
keywords would be better. `nonfinal` still has its own problems of seeming
more widely applicable than it is...)
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.)
>
Having a hard time understanding this part. Trying to map your FNFE analogy
over here, I get something like "You can pattern-match on the Sub type in a
separate case from Super if you want, but don't have to." But I don't see
how it's "knowing about unsealedness" that gives you that; isn't it just
"knowing what some of Super's subtypes are", whether Super is sealed or not?
> *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.)
>
If any subtype is less accessible than whatever access level Javadoc is
building for, it's name really should not be shown, and if it's anonymous
(if that's even allowed), then it's name *can't* even be shown. I think
that all the reader of the doc needs to know is "if matching all the listed
subtypes, do I or don't I also need a case for this type itself?" and that
could happen *either* for the preceding reason or because the type itself
is concrete.
> *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.
>
Whatever you decide here, at least don't think of the current rule as
"silly". It's not silly! It makes code findable. We all do things like
click through github to find source for a class, etc. We should think very
very hard before violating that. Users have two choices already; the fact
that sometimes neither of the two existing options is perfect doesn't
necessarily mean we need a third option. It might just mean that sometimes
you just grumble and accept the lesser of the two evils.
*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.
>
There might be some problem with this I guess, but right now it appeals to
me very much.
--
Kevin Bourrillion | Java Librarian | Google, Inc. | kevinb at google.com
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.openjdk.java.net/pipermail/amber-spec-experts/attachments/20181129/e815bc32/attachment-0001.html>
More information about the amber-spec-experts
mailing list