Hi Paul,

1. A 100% continue with "...Groovy's goal of minimising boilerplate and
   not assuming its users need excessive hand holding" from my side,
   plz G-)
2. non-sealed: Hyphenated & negation keywords indeed seem like a poor
   choice to me, unless they are the counterpart to a non-non-keyword
   (i.e. "sealed" in this case), in which case they represent a
   structured syntax approach which imho is a good idea, IFF the non-*
   flavor is the default (which, as I said above, I would strongly
   support for "sealed").
3. Supporting annotation style seems like a good idea.
4. Definitely Class[] over String[], if doable (using String in these
   cases always felt/feels like an ugly hack to me)

Cheers,
mg


On 31/07/2021 08:08, Paul King wrote:
Hi folks,

With JDK17 introducing sealed classes[1] (which has been in preview
for JDK15/16), Groovy needs to at least properly handle such classes,
i.e. as a minimum not allow such classes to be extended in Groovy.
Otherwise, created classes will give an IncompatibleClassChangeError
when loaded.

The GEP[2] and related PR[3] proposes an approach to both handle that
scenario and also support (almost) the same feature on earlier JDKs
using annotations.

I am interested in any feedback and a couple of points in particular
which are covered later.

Just to summarise, sealed classes provide a way to create a hierarchy
of related classes where further extension beyond the hierarchy is
limited. Public classes are open to extension by anyone. Final classes
are closed for extension. A sealed class hierarchy can be thought of
as providing a mechanism between these two extremes where extension is
open within the hierarchy but closed (or restricted) outside of the
hierarchy. As an initial example, we could create a shape hierarchy of
just circles and squares as follows (using the annotation syntax):

     @Sealed(permittedSubclasses='Circle,Square') interface Shape { }
     final class Circle implements Shape { }
     final class Square implements Shape { }

Or if in the same source file, just:

     @Sealed interface Shape { }
     final class Circle implements Shape { }
     final class Square implements Shape { }

Attempts to create further children will result in compilation errors.

Sealed hierarchies are useful when creating certain kinds of ADTs:

     @Sealed interface Tree<T> {}
     @Singleton final class Empty implements Tree {
         String toString() { 'Empty' }
     }
     @Canonical final class Node<T> implements Tree<T> {
         T value
         Tree<T> left, right
     }

We can have "enhanced" enum-like use cases:

     @Sealed abstract class Weather { }
     @Immutable class Rainy extends Weather { Integer expectedRainfall }
     @Immutable class Sunny extends Weather { Integer expectedTemp }
     @Immutable class Cloudy extends Weather { Integer expectedUV }
     def forecast = [new Rainy(12), new Sunny(35), new Cloudy(6)]

We can mix with traits:

     interface HasHeight { double getHeight() }
     interface HasArea { double getArea() }

     @SelfType([HasHeight, HasArea])
     @Sealed(permittedSubclasses='UnitCylinder,UnitCube')
     trait HasVolume {
         double getVolume() { height * area }
     }

     final class UnitCube implements HasVolume, HasHeight, HasArea {
         // for the purposes of this example: h=1, w=1, l=1
         double height = 1d
         double area = 1d
     }

     final class UnitCylinder implements HasVolume, HasHeight, HasArea {
         // for the purposes of this example: h=1, diameter=1
         // radius=diameter/2, area=PI * r^2
         double height = 1d
         double area = Math.PI * 0.5d**2
     }

     assert new UnitCube().volume == 1d
     assert new UnitCylinder().volume == 0.7853981633974483d

And we can have cases where further extension is not allowed (final),
or permitted either in an open (@NonSealed) or controlled (@Sealed)
way:

     @Sealed(permittedSubclasses='Circle,Polygon,Rectangle') class Shape { }
     final class Circle extends Shape { }
     @NonSealed class Polygon extends Shape { }
     final class Pentagon extends Polygon { }
     @Sealed(permittedSubclasses='Square') class Rectangle extends Shape { }
     final class Square extends Rectangle { }

Notes (covered further in GEP-13):
* An extension (and indeed likely soon) will be to extend the grammar
to support 'sealed', 'non-sealed' and 'permits' keywords. As mentioned
below, the keywords would be Groovy 4+ only.
* The current intention is that the annotation will allow sealed
classes to work from JDK8+ but would only recognise Java17 sealed
classes and produce Java17 compatible bytecode when used on JDK17+.

The current PR marks the annotations as @Incubating, so we have some
time to alter direction slightly if we merge the PR but want to later
change some of the details but it would be good to zero in on desired
behavior as quickly as possible, hence the following
questions/discussion points.

Questions/Discussion points:
(1) The annotations would allow some or all of the initial change to
be backported to versions of Groovy (e.g. to a future 3.0.x or 3.1.x)
where the grammar change isn't made. Are we happy to support both
styles? For traits, the @Trait annotation still exists, but we promote
use of the keyword variant and regard the annotation as mostly an
internal implementation detail.
(2) The @NonSealed annotation is aligned with what would be the
`non-sealed` modifier. Other forums have discussed the hyphenated
'keyword' as being a little ugly. Kotlin uses 'open' and defaults to
classes being final. Scala uses sealed and final but just regards no
modifier as being the non-sealed case. Java designers thought that in
general, non-sealed would be the least used option and didn't want to
align that with no modifier (for fear of developers accidentally
choosing that option accidentally). Do we follow Scala's lead and
align omitting the non-sealed modifier as designating open
inheritance? (This aligns somewhat with Groovy's goal of minimising
boilerplate and not assuming its users need excessive hand holding).
Do we have an alias like unsealed (and @Unsealed) for those wanting to
avoid the "ugly" hyphenated keyword? Java suggests it may likely have
further such keywords in the future in any case.
(3) We could use Class[] rather than String[] for permittedSubclasses
or also support permittedSubclassNames or similar.
(4) Java17 will produce an error if not all of the classes mentioned
in the permits clause are found at compilation time. Currently the PR
doesn't attempt to do this.
(5) Like Java17, this PR opens the possibility of compilers (perhaps
just for type checked code in Groovy's case) to check for
exhaustiveness if sealed hierarchies are used in switch
expressions/statements but doesn't currently implement this feature.

Any thoughts?

Thanks, Paul.

[1] https://openjdk.java.net/jeps/409
[2] 
https://github.com/apache/groovy-website/blob/asf-site/site/src/site/wiki/GEP-13.adoc
(Draft)
[3] https://github.com/apache/groovy/pull/1606

Reply via email to