Search code examples
kotlingenericssupertype

Kotlin Generics: why is the supertype of "SomeActor : DataActor<SomeData>" "DataActor<out SomeData>" and not "DataActor<SomeData>"?


I have build a minimal example to show my problem. I already know a ugly solution but I would like to find a better solution.

Line of Interest: 29: val actor: DataActor<out Data> = when ... The addition of "out" makes it impossible to call functions that take Data as argument (e.g line 36).

With this question question I was able to understand what the problem is: If the type would be DataActor<Data> It would be possible to add any Data to any Actor. E.g it would be possible to add Text to NumberActor. Because I only use data produced by SomeActor as consumable for SomeActor I know it save to do.

But what the question does not answer: What's the idiomatic way to impl. my desired functionality?

The only solution without unchecked casts I can come up with is to do all actions inside the when clause but in the real application this would lead to massive code duplication.

interface Data
class Text(var text: String) : Data
class Number(var number: Int) : Data

interface DataActor<A: Data> {
    fun add(element: A): Boolean;
    fun get(index: Int): A
}

class TextActor : DataActor<Text> {
    private val list = ArrayList<Text>()
    override fun add(element: Text) = list.add(element)
    override fun get(index: Int): Text = list[index]
}
class NumberActor : DataActor<Number> {
    private val list = ArrayList<Number>()
    override fun add(element: Number) = list.add(element)
    override fun get(index: Int): Number = list[index]
}
enum class Selector {
    Number,
    Text
}

fun main() {
    val selector = Selector.Number

    // Why is it from type DataActor<out Data> and not DataActor<Data>?
    val actor = when(selector) {
        Selector.Number -> NumberActor()
        Selector.Text -> TextActor()
    } // as DataActor<Data> // ugly solution

    val data = actor.get(0)
    // This is not possible as it needs DataActor<in Data> or DataActor<Data>
    actor.add(data)
}

Solution

  • I think you explained you already understand the problem. These two classes do not have the same invariant supertype. They can only have a common covariant supertype.

    So to solve this, you need to break out the code that maintains invariance into its own generic function. Within the generic function, T is invariant. It doesn't care which T it is, so the code is safe.

    fun main() {
        val selector = Selector.Number
    
        val actor = when(selector) {
            Selector.Number -> NumberActor()
            Selector.Text -> TextActor()
        } 
    
        doWork(actor)
    }
    
    fun <T: Data> doWork(actor: DataActor<T>) {
        val data = actor.get(0)
        actor.add(data)
    }
    

    This of course is based on your very simple example. This solution may not be feasible for more complicated cases, like if you were trying to transfer items between distinct instances of these classes.