JEP-360 Sealed types and non-accessible subtypes
Brian Goetz
brian.goetz at oracle.com
Sun Dec 1 18:28:50 UTC 2019
> Right now there is only limited support for this: one can define a public abstract class with non-public constructor.
Correct, this works for classes, but not for interfaces. This puts API designers in a bind: it is better to expose only interfaces and final classes to clients, but without sealed types, API designers have a choice of allowing arbitrary implementation (which has costs to the API implementation) or exposing abstract classes. Sealed types give them the best of both worlds.
> The syntax requires to explicitly list all permitted subtypes.
> It could be possible to make permits clause optional. Omitting it would mean "all subtypes in the same module".
> I admit such syntax is just a nice to have as explicitly listing subclasses is not such a big deal
> when all types must be in the same module anyway.
The permits clause is optional, but in that case is restricted to inferring all classes in the _same compilation unit_. This means that while the permits clause can be omitted from the source code, a full explicit list is available in the class file anyway. In turn, this enables the compiler to reason about _exhaustiveness_ — that it knows what all the subtypes are.
This is a common thing that people miss about sealed types; they think about it only in terms of “I want to restrict my subtypes.” Which is part of it, but another part is being able to _enumerate_ the subtypes of a given class at the time a client is compiled — which enables the compiler to reason about exhaustiveness (say, in switches with type patterns.) So while it might be tempting to say “permits package” or “permits module”, such a use would be giving up a benefit without necessarily realizing it.
> What is interesting though is the interaction of permitted types declaration and exhaustiveness check:
> how should it work in case when a public sealed type permits a non-public/non-exported type?
This works as you would expect. When you compile against a JAR, you can _see_ the private classes in that API, but you can’t access them. If you try to access them (at compile or run time), you get an error. Sealed types work with this mechanism. If type A is sealed to permit B and C, if both B and C are accessible to the client trying to do an exhaustive switch on A, then the client is rewarded by being able to write a total switch on A. If B is accessible but not C, the compiler knows that the switch is _not_ exhaustive, and will make you write a default clause if you want to be total. So you still get the full type checking of the exhaustiveness, but you may or may not be able to explicitly match on all the types.
This means that sealing and accessibility interact in basically the obvious way from the perspective of an API designer; if a class is sealed but some of the subtypes differ in their accessibility, that means that some clients may be able to switch explicitly and exhaustively, and others may not be.
When we have full pattern matching, we expect that a not-uncommon scenario will be where there is a sealed public interface and a number of private subtypes, with public patterns exposed instead. That means the implementation gains the ability to assume that it controls all implementations, but without the obligation to expose them all.
> Introduction of exhaustiveness check also means adding a new permitted subtype is an incompatible change
> - similar to adding a value to existing enum.
Yes, this is exactly analogous to the enum case. When the compiler identifies that a switch is exhaustive, it compiles in a default case anyway, which throws the obvious exception. What makes this an improvement over just providing the default yourself is not the convenience (though that is nice), but the fact that you will no long sweep these separate-compilation issues under the rug. In this situation, when you go to _recompile_ the client, and a new type/enum has been added, you’ll get a compile-time error that tells you that you are no longer exhaustive, and you get a chance to deal with it rather than sweeping it into the default that you think will never be matched.
> This makes it somewhat less useful for the module implementation only types use case.
> The solution might be to have sealed types without explicit permits clause (see above) and
> such types could not be used for exhaustiveness checks.
This is exactly why we did not allow “permits package” or “permits module”, and tied implicit permits to inferring the subtypes in the same compilation unit.
More information about the amber-dev
mailing list