kotlingenericsnested-generics

How do I write Nested Generics in Kotlin's Sealed Classes


I'm trying to come up with a data model that allows me to do the following:

Initially, I came up with this data model:

sealed class Task<R : TaskResult> {
    
    abstract val status: TaskStatus<R>

    data class A(
        val data: String,
        override val status: TaskStatus<NoResult>,
    ) : Task<NoResult>()

    data class B(
        val data: String,
        override val status: TaskStatus<TaskBResult>,
    ) : Task<TaskBResult>()
}

sealed class TaskStatus<R : TaskResult> {
    object InProgress : TaskStatus<NoResult>()
    data class Completed<R : TaskResult>(val result: R) : TaskStatus<R>()
}

sealed class TaskResult {
    object NoResult : TaskResult()
    data class TaskBResult(val resultData: String) : TaskResult()
}

Here you have Task.A and Task.B, where:

However, when I run this:

fun main() {
    val taskA = Task.A(
        data = "data",
        status = TaskStatus.InProgress
    ).copy(
        status = TaskStatus.Completed(
            result = NoResult
        )
    )

    val taskB = Task.B(
        data = "data",
        status = TaskStatus.InProgress
    ).copy(
        status = TaskStatus.Completed(
            result = TaskBResult(
                resultData = "resultData"
            )
        )
    )
}

I get the following compile error for setting the initial status of Task.B:

status = TaskStatus.InProgress
    Type mismatch.
    Required: TaskStatus<TaskResult.TaskBResult>
    Found: TaskStatus.InProgress

Does anyone know how to change the data model so I'm allowed to run this (or a very similar) main function?


Solution

  • This could work with a very little change: just make TaskStatus a covariant generic class and make InProgress a TaskStatus<Nothing>. This is a typical strategy you can use when you have "special case" objects that represent no state. After this change, your code should compile:

        sealed class Task<R : TaskResult> {
    
            abstract val status: TaskStatus<R>
    
            data class A(
                val data: String,
                override val status: TaskStatus<TaskResult.NoResult>,
            ) : Task<TaskResult.NoResult>()
    
            data class B(
                val data: String,
                override val status: TaskStatus<TaskResult.TaskBResult>,
            ) : Task<TaskResult.TaskBResult>()
        }
    
        sealed class TaskStatus<out R : TaskResult> {
            object InProgress : TaskStatus<Nothing>()
            data class Completed<R : TaskResult>(val result: R) : TaskStatus<R>()
        }
    
        sealed class TaskResult {
            object NoResult : TaskResult()
            data class TaskBResult(val resultData: String) : TaskResult()
        }
    
    fun main() {
        val taskA = Task.A(
            data = "data",
            status = TaskStatus.InProgress
        ).copy(
            status = TaskStatus.Completed(
                result = NoResult
            )
        )
    
        val taskB = Task.B(
            data = "data",
            status = TaskStatus.InProgress
        ).copy(
            status = TaskStatus.Completed(
                result = TaskBResult(
                    resultData = "resultData"
                )
            )
        )
    }