I'm interested in a use case for a restricted clas...
# language-evolution
j
I'm interested in a use case for a restricted class hierarchy where the sealed superclass is able to be instantiated directly. An example would be an immutable superclass and mutable subclass, which shouldn't be subclassed outside a library. Currently such use cases require using an open superclass and final subclass, which can't prevent subclassing the superclass externally. This change to expect/actual in Kotlin 1.9.20 introduces the need to avoid using `expect open class`es, so I've been exploring alternatives to adapt my code that uses this pattern currently. Continued in đź§µ.
Sealed classes require their constructors to be private or protected to prevent being called from an external subclass. But is there actually a need for the class to be abstract, or is this just by convention, as the typical sealed class use cases only care about instantiating the concrete subclasses? My workaround for creating public or internal constructors for a sealed class is to use either a fake factory function constructor, or
companion object
operator fun invoke()
, but I also need to instantiate a dummy subclass implementation to overcome the sealed class being abstract:
Copy code
sealed class Foo(param: Unit)
// Would be nice to avoid needing this
// class and instantiate Foo directly
private class FooImpl : Foo(Unit)
fun Foo(): Foo = FooImpl()
class MutableFoo : Foo(Unit)
However, this then breaks certain uses where you might access an object's class instance. It will be
FooImpl
and not the expected
Foo
. Is there a fundamental reason sealed classes must be abstract?
c
Is there a fundamental reason sealed classes must be abstract?
They don’t have to be abstract.
Copy code
sealed class BaseClass {
    class Foo : BaseClass()
    data object Bar : BaseClass()
}
…but they cannot be instantiated:
Copy code
Cannot access '<init>': it is protected in 'BaseClass'
Sealed types cannot be instantiated
What is it you are trying to accomplish?
j
From the docs:
A sealed class is abstract by itself, it cannot be instantiated directly and can have
abstract
members.
I described an example of a use case. An immutable superclass and mutable subclass. Both are concrete and can be instantiated directly. But neither should be subclassed outside the library's module.
c
Without context as to the underlying problem that pattern solves this may suffice - two different states, one immutable, one not, both of type BaseClass.
Copy code
sealed class BaseClass {
    data object Foo(val x : Int) : BaseClass()
    data object Bar(var something : String) : BaseClass()
}
j
MutableFoo
subclasses
Foo
because it uses all of its immutable functionality. They're not siblings, but parent/child for a reason.
c
âť“ there was no MutableFoo in the example.
Foo
is an object (or could be a class) with mutable state.
Well, its
Bar
that is mutable.
Foo
is immutable.
j
there was no MutableFoo in the example
Look at the last line of my second post with the example.
Foo
is a class also in the example. It's not a singleton object.
đź‘€ 1
c
In you example wondering why the need for an instance of
Foo
directly, instead of another subtype in the sealed hierarchy?
j
Because this is the way the API currently works. There's no need for introducing an abstract class or interface to the API, other than needing to overcome the limitation of sealed classes being abstract.
My current implementation, utilizing an open superclass and final subclass, has worked ok. But this is in a KMP project, and as I mentioned, this upcoming change in Kotlin 1.9.20 makes using `expect open class`es much more difficult and ideally should be avoided as a result. So I'm looking for alternatives, one of which is to use a sealed class hierarchy, except sealed classes don't support this use case well, without being able to directly instantiate the abstract superclass as I've described.
c
Unclear on what that API looks like like - perhaps a concrete example? Are these return types from API calls or something else?
does this work with preventing external subclassing?
Copy code
open class Foo internal constructor(x : Int)
final class MutableFoo(x : Int) : Foo(x)
j
A concrete example is
Array
and
MutableAray
. Another is
Dictionary
and
MutableDictionary
. The immutable versions can't be instantiated publicly, but are returned from and passed to many APIs. They do require
internal
constructors to implement in my KMP library. The mutable versions can be instantiated publicly.
c
right. so on the superclass that you don’t wan to be publicly instantiable, make the constructor
internal
.
j
does this work with preventing external subclassing?
While it may prevent subclassing externally, it still breaks this new expect/actual KMP requirement, as common and platform source sets are compiled separately. So there's no way for the compiler to know you didn't subclass in common or use any of the same additional API properties or method as were added in the platform implementation.
right. so on the superclass that you don’t wan to be publicly instantiable, make the constructor
internal
.
This is what I'm currently doing, but can no longer do with KMP expect/actual in Kotlin 1.9.20.
A more concrete expect/actual example looks like this:
Copy code
// common
expect sealed class Foo
expect class MutableFoo() : Foo

// platform
actual sealed class Foo(
    internal open val impl: FooPlatformImpl
)

// Would be nice to avoid needing this
// class and instantiate Foo directly
private class FooImpl(
    impl: FooPlatformImpl = FooPlatformImpl()
) : Foo(impl)

internal fun Foo(): Foo = FooImpl()

actual class MutableFoo(
    override val impl: MutableFooPlatformImpl
) : Foo(impl) {
    actual constructor() : this(MutableFooPlatformImpl())
}
As I've played with this some more, even `expect sealed class`es look to have the same problem as `expect open class`es with this change, as they are still non-final. So I'll need to explore other solutions.