Sealed types -- updated proposal

Remi Forax forax at univ-mlv.fr
Thu Jan 17 10:32:50 UTC 2019


I'm still not 100% sure that mixing the exhaustiveness and the closeness is a good idea, 
again because 
- you may want closeness of non user named types 
- you may want exhaustiveness not only types (on values by example) 
but it makes the feature simple, so let's go that way. 

Allowing public auxillary subtype of a primary sealed type is the sweet spot for me, better than trying to introduce either a nesting which is not exactly nesting or a rule than only works for pattern matching. 

I don't understand how "semi-final" can be a good keyword, the name is too vague. Given that the proposal introduce the notion of sealed types, "sealed" is a better keyword. 
For un-sealing a subtype, "unsealed" seems to be a good keyword. 

Rémi 

> De: "Brian Goetz" <brian.goetz at oracle.com>
> À: "amber-spec-experts" <amber-spec-experts at openjdk.java.net>
> Envoyé: Mercredi 9 Janvier 2019 19:44:12
> Objet: Sealed types -- updated proposal

> Here's an update on the sealed type proposal based on recent discussions.

> 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 semi-final
> modifier to a class, abstract class, or interface:
> semi-final interface Node { ... }

> In this streamlined form, Node may be extended only by named classes declared in
> the same nest. This may be suitable for many situations, but not for all; in
> this case, the user may specify an explicit permits list:
> semi-final interface Node
>     permits FooNode, BarNode { ... }

> Note: permits here is a contextual keyword.

> 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 nest.

> 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.
> Permitted subtypes must belong to the same module (or, if not in a module, the
> same package.)

> 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_FINAL
> modifier, and a PermittedSubtypes attribute which contains a list of permitted
> subtypes (similar in structure to the nestmate attributes.)

> Transitivity. Sealing is transitive; unless otherwise specified, an abstract
> subtype of a sealed type is implicitly sealed (permits list to be inferred),
> and a concrete subtype of a sealed type is implicitly final. This can be
> reversed by explicitly modifying the subtype with the non-final modifier.

> 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.)

> Note: Scala made the opposite choice with respect to transitivity, requiring
> sealing to be opted into at all levels. This is widely believed to be a source
> of bugs; it is rare that one actually wants a subtype of a sealed type to not
> be sealed. I suspect the reasoning in Scala was, at least partially, the desire
> to not make up a new keyword for “not sealed”. This is understandable, 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:
> semi-final interface ConstantDesc
>     permits String, Integer, Float, Long, Double,
>             ClassDesc, MethodTypeDesc, MethodHandleDesc,
>             DynamicConstantDesc { }

> semi-final interface ClassDesc extends ConstantDesc
>     permits PrimitiveClassDescImpl, ReferenceClassDescImpl { }

> private class PrimitiveClassDescImpl implements ClassDesc { }
>  private class ReferenceClassDescImpl implements ClassDesc { }

> semi-final interface MethodTypeDesc extends ConstantDesc
>      permits MethodTypeDescImpl { }

>  semi-final interface MethodHandleDesc extends ConstantDesc
>      permits DirectMethodHandleDesc, MethodHandleDescImpl { }

> semi-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.

> Accessibility. Subtypes need not be as accessible as the sealed parent. In this
> case, not all clients will 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 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.)

> Auxilliary subtypes. 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.

> One way to do get there would be to relax the “no public auxilliary classes”
> rule to permit for sealed classes, say: allowing public auxilliary subtypes of
> the primary type, if the primary type is public and sealed.

> Another would be to borrow a trick from enums; for a sealed type with nested
> subtypes, when you import the sealed type, you implicitly import the nested
> subtypes too. That way you could declare:
> semi-final interface Node {
>     class A implements Node { }
>     class B implements Node { }
> }

> ​but clients could import Node and then refer to A and B directly:
> switch (node) {
>     case A(): ...
>     case B(): ...
> }

> We do something similar for enum constants today.
>-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.java.net/pipermail/amber-spec-experts/attachments/20190117/5286b115/attachment-0001.html>


More information about the amber-spec-experts mailing list