What’s the official plan with sealed inner classes? According to this issue, this issue, a couple other random issues, and this thread it seems like they might be on their way out. Is that correct? They seem pretty handy to me, especially in the generics case referenced in the linked thread (of which I have a similar example in my own code).
In kotlin 1.0, where subclasses of sealed classes can be declared only inside such class inner sealed classes has no sense (according KT-16233). In 1.1 we allow subclasses for sealed classes outside sealed classes, but we do it only for top-level sealed classes for now.
If we allow it for not top-level sealed classes, then we will allow inner sealed classes too.
But it is open design issue, and I’m almoust sure, that in 1.2 there will be no changes in this area.
Note that you can declare inner class with private constructor and it should partially solve your use-case.
Alright, thanks for your response. I’m not really sure what I can offer, other than use cases I suppose.
One pattern that seems like it would be super useful is to use sealed inner classes with coroutine actors. Here’s a stripped-down version of a class I’ve been toying with:
class BroadcastChannel<T>(context: CoroutineContext = CommonPool) {
private sealed inner class Msg {
inner class Subscribe(val channel: SendChannel<T>): Msg()
inner class Broadcast(val obj: T): Msg()
}
private val mainActor = actor<Msg>(context) {
val subscribers = linkedSetOf<SendChannel<T>>()
for (msg in channel) {
when (msg) {
is Msg.Subscribe -> subscribers.add(msg.channel)
is Msg.Broadcast -> subscribers.forEach { it.send(msg.obj) }
}
}
}
suspend fun subscribe(channel: SendChannel<T>) {
mainActor.send(Msg.Subscribe(channel))
}
suspend fun send(obj: T) {
obj!! // Seems to think obj is nullable otherwise
mainActor.send(Msg.Broadcast(obj))
}
}
This fails primarily because Msg.Subscribe()
doesn’t work – it needs an instance of Msg
to instantiate Subscribe
. Adding an object
inside Msg
doesn’t work either because it doesn’t really make sense, plus KT-16232. Even if Msg
wasn’t sealed we’d still have this problem, though.
Changing
private sealed inner class Msg {
inner class Subscribe(val channel: SendChannel<T>): Msg()
inner class Broadcast(val obj: T): Msg()
}
to
private sealed inner class Msg
inner class Subscribe(val channel: SendChannel<T>): Msg()
inner class Broadcast(val obj: T): Msg()
doesn’t work for the reason you mentioned already – the compiler simply doesn’t allow it.
However, this does work: (and is what I think you were suggesting)
private open inner class Msg
private inner class Subscribe(val channel: SendChannel<T>): Msg()
private inner class Broadcast(val obj: T): Msg()
The main unpleasantness being that Msg
is now open
when it doesn’t really need to be, and for organizational reasons I’d like Subscribe
and Broadcast
to be grouped together somehow, but this isn’t a requirement.
I don’t understand why Msg has to be inner in this code. Please, explain. It seems that it will work just as expected if you just drop inner modifier.
The T
generic type doesn’t work unless it’s inner.
Ah. Makes sense. Thank you. The solution I would suggest is to remove inner
modifier, but make Msg
generic in T
instead. The following code seems to compile as needed:
class BroadcastChannel<T>(context: CoroutineContext = CommonPool) {
private sealed class Msg<T> {
class Subscribe<T>(val channel: SendChannel<T>): Msg<T>()
class Broadcast<T>(val obj: T): Msg<T>()
}
private val mainActor = actor<Msg<T>>(context) {
val subscribers = linkedSetOf<SendChannel<T>>()
for (msg in channel) {
when (msg) {
is Msg.Subscribe -> subscribers.add(msg.channel)
is Msg.Broadcast -> subscribers.forEach { it.send(msg.obj) }
}
}
}
suspend fun subscribe(channel: SendChannel<T>) = mainActor.send(Msg.Subscribe(channel))
suspend fun send(obj: T) = mainActor.send(Msg.Broadcast(obj))
}
The only non-trivial problem with this code, is that IDEA correctly reports in Msg<T>
that T
is not used, however you cannot simply remove it, or you will not be able to nicely use when
on it (you’ll have to do unchecked casts).
Okay, this technically works (and is probably what I’ll end up doing), but now the T
in Msg<T>
isn’t actually the same T
as BroadcastChannel<T>
's T
(meaning I think Msg<T>
is defining a new T
type). However, that actually doesn’t seem to matter, but it’s mildly confusing – the following works just fine:
class BroadcastChannel<T>(context: CoroutineContext = CommonPool) {
private sealed class Msg<Ignored> { // can be generic on...anything
class Subscribe<T>(val channel: SendChannel<T>): Msg<T>()
class Broadcast<T>(val obj: T): Msg<T>()
}
// snip
}
So Subscribe
can continue to have a generic type that matches the outer T
. Cool, good enough for now. Thanks!
The declaration of Msg<T>
indeed introduces a new generic type type T
that is unrelated to T
in BroadcastChannel<T>
at first. However, the following declaration is what unites them:
mainActor = actor<Msg<T>>
It declares that an actor is going to listen to messages of type Msg<T>
, where this T
is coming from T
in BroadcastChannel<T>
.
@elizarov Your suggestion to add <T>
to Msg
class declaration works, but please note that this is a very simple case. When the BroadcastChannel
class generic parameters are more complex, for example…
class BroadcastChannel<
SomeType1 : Foo<SomeType1>,
SomeType2 : Foo<SomeType2>,
...,
>
…and you have more than one nested class that needs access these type parameters, then “inlining” them into every single nested class doesn’t scale well.