Sealed types
Brian Goetz
brian.goetz at oracle.com
Fri Nov 30 01:42:58 UTC 2018
>
> 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.
All these features work nicely together, but the kinship I was thinking of was that sealed types are sum types, and records are product types, and sums of products are a particularly useful pattern. But, you are right that to really get the benefit, you also want pattern matching over the sum. In reality, all of these features are good on their own and better with each other, so this is a minor point.
>
> 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?).
I worry that requiring this will feel heavyweight, especially when the list is long AND the classes being declared are compact (like records). Take the following example, and scale it to 26 types; I can easily imagine being irritated that I have to list out A .. Z twice.
sealed interface X
permits A, B, C { }
record A() implements X {}
record B(int b) implements X { }
record C(int c, int d) implements X { }
But, as a style suggestion, it becomes more reasonable; I could easily imagine the JDK doing this just for clarity, as we tend to be less concerned with concision than the average developer.
>
> 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.
In nearly all the use cases I have in mind, either the subtypes are declared all together, or they are strewn about the file system. I have a harder time imagining the case where you have 20 that are co-declared, and one that is elsewhere; that’s the case in which the additive interpretation would pay for itself. Can you think of situations in which this would arise regularly?
My concern is not about the source misusing itself, as much as the reader being able to reason about it more easily. I like the idea that if there is a permits clause, it is exhaustive; if you leave it out, the compiler infers the obvious thing. Much less to reason about when reading code. If the source file is large, readers shouldn’t have to trawl through the whole file just to find out “permits A, B” isn’t an exhaustive list.
>
>
> 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?
If we plunk for the bit (these bits are expensive), then there will surely be legacy classfiles that say FINAL but not SEALED, so we have to be prepared to see that. If we merge the bits, the risk is that classfile interpreters may (reasonably) interpret final as “no subtypes”, even thought the classfile is strongly versioned. I think I err on the side of bit preservation.
>
>
> 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.)
An unsealed subCLASS of a sealed super type can be a very useful move (see DynamicConstnatDesc in JEP-334), but an unsealed subINTERFACE is harder to grok, because interfaces (absent sealing) lack the ability to constrain their subclasses very much (no protected members, no final methods, no nonpublic constructors.)
>
> 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.
Your confusion is a good argument to consider the syntactic choice of retconning final, rather than adding sealing :)
The idea is that without utterance to the contrary, a subINTERFACE of a sealed type is implicitly sealed, and a subCLASS of a sealed type is implicitly final. This could be reversed by uttering the non-{sealed,final}, incantation. (You could also explicitly re-seal a subtype, which would be useful to provide a permits list.)
>
> (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…)
The rationale here is that it is reasonable to assume that we might at some point want the opposites of modifiers like abstract, final, static, etc. We could of course make up ad-hoc names, but not only is that more names and more tokens the parser has to conditionally treat specially, but even for the opposite of “final” we would probably need two, since final means both “immutable” and “non-subclassable/overridable”, and its pretty hard to think of a word that covers both (mutable is good for the first; open is good for the second.) Having a mechanical rule also reduces the degree of bike shed debate. Hence, my preference for this approach, but I understand it might not be universally admired.
(But, Kevin is right — not the place for this discussion. Everyone, please don’t weigh in on this further now.)
>
> 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?
Right, just saying that having a unsealed subtype doesn’t burden clients with having to know the details if they don’t want. They can pretend everything is sealed, and still exhaustively switch over it.
>
>
> 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.
Reasonable. The key is that the doc should say “there may be others”, so that users don’t get surprised when their seemingly-exhaustive switch fails.
>
> 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 finalavoids 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.
>
Yeah, me too.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.openjdk.java.net/pipermail/amber-spec-experts/attachments/20181129/a498cced/attachment-0001.html>
More information about the amber-spec-experts
mailing list