Sealed Class Hierarchies and when-expressions

Goals:

  • Make sealed class hierarchies possible (this implies more exhaustiveness criteria for when-expressions)

Discussion

Sealed Class Option

This option is chosen as final.

Special sealed keyword is introduced to mark base classes for sealed hierarchies, like this:

sealed class Type () {
    class Named(val name: String) : Type()
    class Nested {
        class Function(val param: Type, val result: Type): Type()
    }
    object Top: Type()
}
// ...
when (type) {
    is Named -> println(name)
    is Nested.Function -> println("$param -> $result")
    // Alternatively, we can omit is here
    is Top -> println("TOP")
}

It's assumed here that sealed class can be subclassed only by nested classes or objects, both with any level of nesting, but not by local classes of any sort. So the when example would operate correctly.

Sealed classes are abstract by default, so abstract modifier is redundant (otherwise when requires additional case covering all other cases: is Type). Sealed classes can never be open or final. Sealed interfaces are prohibited, otherwise Java classes could easily inherit them. Sealed objects are also not possible, because we cannot inherit from object.

Private Constructor Option

Another option would be to make when understand hierarchies like this one:

abstract class Type private () {
    class Named(val name: String) : Type()
    class Function(val param: Type, val result: Type): Type()
    object Top: Type()
}

When the superclass has a private constructor, all its subclasses are known to be (directly or indirectly) nested into in, and we can be sure that a set of is-checks is exhaustive if all the subclasses are included:

when (type) {
    is Named -> println(name)
    is Function -> println("$param -> $result")
    is Top -> println("TOP")
}

However, class with a private constructor can also be derived as a local class, that provides a problem for this option implementation.

Future When Optimization

It's possible to optimize when on sealed in the way like when on enum or when on string. There are the following opportunities, all of them use KSealed interface with some final identification method:

  • use ordinal() method, which is implemented like enums, so the first descendant has an ordinal 0, second 1 and so on. When on sealed organized like when on enum. Drawback: reordering breaks client's code.
  • use sealedName() method returning a fully qualified class name of a direct sealed descendant. When on sealed organized like when on string. Drawback: extra efforts.
  • use sealedId() method returning a hash code of a direct sealed descendant fully qualified class name. Drawbacks: possible collisions, including an opportunity to rename some member and get hash code of another member, which breaks client's code in a dramatic way.

After optimization, instanceof KSealed should be checked at run-time before applying it.