Home > Software design >  How do I write Nested Generics in Kotlin's Sealed Classes
How do I write Nested Generics in Kotlin's Sealed Classes

Time:06-21

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

  • Define a type of Task and change its Status
  • This Status can be either InProgress or Completed
  • In the case of a completed Status, I want to be able to add data, that is specific to the Task that was completed.

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:

  • A completed Task.A only accepts NoResult
  • A completed Task.B only accepts TaskBResult

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?

CodePudding user response:

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"
            )
        )
    )
}
  • Related