Sealed inner classes


#1

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


#2

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.


#3

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.


#4

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.


#5

The T generic type doesn’t work unless it’s inner.


#6

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


#7

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!


#8

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