Generic-type-specific sealed class subclasses

I have a scenario where two functions return almost the same sealed result classes, with a few exception:

  • when the operation is successful, the result class wraps a different type of result. This is not a problem, can be easily solved by using a generic for the sealed class.
  • there are specific errors that depend on the type of result. This is where the problem appears, since this specific error has to be handled even when its generic type is incompatible with the declared generic type of the sealed class that the function returns.

Something like this:

sealed class Result<out T>
object GenericError1 : Result<Nothing>()
object GenericError2 : Result<Nothing>()
class Success<T>(val result: T) : Result<T>()
object StringError : Result<String>()

fun returnStringResult() : Result<String> {
      return Success("success")
}

fun returnFloatResult() : Result<Float> {
    return Success(234324.23432f)
}

// The following functions include a return statements before when so that the when has to be exhaustive
fun messageForStringResult(): String {
    return when (val result = returnStringResult()){
        is GenericError1 -> {
            println("Generic error 1 occurred")
            "Generic error 1"
        }
        is GenericError2 -> {
            println("Generic error 2 occurred")
            "Generic error 2"
        }
        is Success -> {
            result.result
        }
        is StringError -> {
            println("String-specific error occurred")
            "String specific"
        }
    }
}

fun messageForFloatResult(): String {
    return when (val result = returnFloatResult()){
        is GenericError1 -> {
            println("Generic error 1 occurred")
            "Generic error 1"
        }
        is GenericError2 -> {
            println("Generic error 2 occurred")
            "Generic error 2"
        }
        is Success -> {
            "Result: ${result.result}"
        }
        else -> {
            "Impossible"
        }
    }
}

In messageForFloatResult, it’s necessary to have the else branch (I want an exhaustive when), but it would be much better if it wasn’t necessary. Note that it’s impossible for this else branch to be executed, because StringError can never be returned from a function that returns Result<Float>.

I’ve done something like this before using inheritance, which might help here.

Here’s what that looks like:

sealed class ValidatedInput {
    object Valid : ValidatedInput()
    abstract class Invalid(val reason: String = "") : ValidatedInput()

    object Zero : Invalid("This cannot be 0.")
    object Negative : Invalid("This cannot be negative.")
    object Empty : Invalid("You must provide input for this value.")
    class Malformed(reason: String) : Invalid(reason)
}

fun malformedInput(how: String) = ValidatedInput.Malformed(how)

Which I used in a few places like this (this is Android code making use of EditText::setError):

when (val validation = viewModel.inputValidation(it.text.toString())) {
    is ValidatedInput.Invalid -> textEntry.error = validation.reason
    is ValidatedInput.Valid -> textEntry.error = null
}

So you could consider doing something similar with Result<out T> like this:

sealed class Result<out T>
data class Success<T>(val result: T) : Result<T>()
abstract class Error<T>(val message: String = "") : Result<T>()

object GenericError1 : Error<Nothing>("Generic Error 1 Occurred")
object GenericError2 : Error<Nothing>("Generic Error 2 Occurred")
class StringError(reason: String) : Error<String>(reason)

You could also substitute the new Result<T> type from the standard library (see here). At the time of writing this using Result<T> as a return type requires the -Xallow-result-return-type compiler arg to opt-in, but I haven’t had any troubles with it.

1 Like