Is Nothing the subclass of R when I use sealed class in Kotlin?

Viewed 129

I often see the following Code A about sealed class in Kotlin.

1: By the structure of the sealed class Result,it seems that Nothing just like the subclass of R, right?

2: I think Code A should be Code B, is it right?

3: And more, it seems that I never see the code just like Code C, is it wrong?

Code A

sealed class Result<out R> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
}

Code B

sealed class Result<out R> {
    data class Success<out R>(val data: R) : Result<R>()
    data class Error(val exception: Exception) : Result<Nothing>()
}

Code C

sealed class Result<out R> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Fail<out K>(val data: K) : Result<K>()
}

Added Content

To Joffrey: Thanks!

In Code 1 , Noting is just look like the subclass of R

I think Code 2 is easy to understand more then Code 1, right?

Code 1

sealed class Result<out R> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
}


val cityDetails: Result<ExploreModel>
        get() {
            val destination = destinationsRepository.getDestination(cityName)
            return if (destination != null) {
                Result.Success(destination)
            } else {
                Result.Error(IllegalArgumentException("City doesn't exist"))
            }
      }

if (cityDetails is Result.Success<ExploreModel>) {
      ...
 } else {
      onErrorLoading()
}

Code 2

 sealed class Result<out R> {
        data class Success<out T>(val data: T) : Result<T>()
        data class Error(val exception: Exception) : Result<Nothing>()
    }

 val destination = destinationsRepository.getDestination(cityName)
 val cityDetails= if (destination != null)  Result.Success(destination) else Result.Error(IllegalArgumentException("City doesn't exist"))

 if (cityDetails is Result.Success<ExploreModel>) {
     ...
 } else {
     onErrorLoading()
}

Add content again:

By the declaration of the class Result, out R and out T can be different type, and out R and out T can be any type.

Why is Code 3 correct, and Code 4 wrong?

Does it mean that T must be the subclass of R in sealed class Result?

BTW, Code 5 can be compiled.

Code 3

sealed class Result<out R> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()

 }

  val cityDetails: Result<ExploreModel>
    get() {
        val destination = destinationsRepository.getDestination(cityName)
        return if (destination != null) {
            Result.Success<ExploreModel>(destination)
        } else {
            Result.Error(IllegalArgumentException("City doesn't exist"))
        }
    }

if (cityDetails is Result.Success<ExploreModel>) {
  ...
} else {
  onErrorLoading()
}

Code 4

sealed class Result<out R> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()

 }

  val cityDetails: Result<String>      //It's String type
    get() {
        val destination = destinationsRepository.getDestination(cityName)
        return if (destination != null) {
            Result.Success<ExploreModel>(destination)
        } else {
            Result.Error(IllegalArgumentException("City doesn't exist"))
        }
    }

 if (cityDetails is Result.Success<ExploreModel>) {
  ...
 } else {
   onErrorLoading()
 }

Code 5

   sealed class Result<out R> {
      data class Success<out T>(val data: T) : Result<T>()
      data class Fail<out K>(val data: K) : Result<K>()
   }

   val aa:  Result1<Any>
          get(){
             return if (true) Result1.Success("Hello") else Result1.Fail(3)
          }

Add content thirdly:

To Joffrey: Thank you very much!

Your answer is great, I can understand completely, thanks again

Anyway, I think the Code Ok is better than the Code 1, it's clear without any ambiguous, right?

Code OK

sealed class Result<out R> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
}


val cityDetails: Result<Any>
        get() {
            val destination = destinationsRepository.getDestination(cityName)
            return if (destination != null) {
                Result.Success(destination)
            } else {
                Result.Error(IllegalArgumentException("City doesn't exist"))
            }
      }

if (cityDetails is Result.Success<ExploreModel>) {
      ...
 } else {
      onErrorLoading()
}
1 Answers

1: By the structure of the sealed class Result,it seems that Nothing just like the subclass of R, right?

The Nothing type is a subtype of every type, because there are no values of that type.

But to say that it's a subtype of R is sort of incorrect, because R is not really defined here. You are declaring a class Error, and you "input" Nothing as a value for the type parameter R of the class Result, just like you would pass arguments to a method.

Nothing is useful when you have a generic type parameter (like R here) from a parent class that you don't use anywhere in the class. It's not meant to be used even in the parent, so Nothing shows it more clearly.

2: I think Code A should be Code B, is it right?

Code A and B are completely identical to the compiler. Those type parameters are just variable names. I guess using a different name avoids "shadowing" and makes it easier to follow.

3: And more, it seems that I never see the code just like Code C, is it wrong?

Code C is not wrong, it just doesn't serve the same purpose. In the first case the failure case encapsulates a Throwable, and in code C it encapsulate a value of a given type. It's totally ok to declare multiple subclasses with type parameters.


Answer to the edited question:

In your updated example, Code 1 and Code 2 don't do the same thing. The first one recomputes the value every time it's accessed, while Code 2 sets it once and never changes it again


Answer to the second edit:

By the declaration of the class Result, out R and out T can be different type, and out R and out T can be any type.

Does it mean that T must be the subclass of R in sealed class Result?

I think you are confusing generic variance and simple subtyping of generic types. They are tightly related, but let me try to clarify.

T and R are type parameters. This means that when you instantiate a Result.Success object, you choose a type for T. Or when you instantiate a Result object, you choose a type for R (it's technically not possible to instantiate Result directly because it's an abstract class, but still).

I think what you're missing is that the T of the Result.Success<T> class is the R of the Success class. This is defined by the declaration:

data class Success<out T>(val data: T) : Result<T>()

This means that instances of Result.Success<T> object are also instances of Result<T> (the same T!). Because Result.Success<T> is a subtype of Result<T> (the same T!).

For instance if you write Result.Success<ExploreModel>(destination) (like in your Code 3), you choose T=ExploreModel, and by definition of this class the instance is also a Result<ExploreModel>.

Similarly, when you write Result.Error(IllegalArgumentException("City doesn't exist")), you create an instance of Result.Error which is also an instance of Result<Nothing> (by definition of the Result.Error class).

Now let's talk about variance. For a given generic type, like Result<R>, you can define a type hierarchy between different "versions" of this type using different types as R.

With the out keyword, you declare Result<out R> as "covariant in R", which means that if Dog is a subtype of Animal, Result<Dog> is a subtype Result<Animal>.

Now this should clarify why this is valid:

val cityDetails: Result<ExploreModel> = Result.Success<ExploreModel>(destination)

But this is not:

val cityDetails: Result<String> = Result.Success<ExploreModel>(destination)

It's not really about T being a subtype of R, it's about R being a subtype of another R if you want to assign a Result<R> instance to a variable of a Result<R> type with a different value of R:

val cityDetailsModel: Result<ExploreModel> = Result.Success<ExploreModel>(destination)

val cityDetailsStr: Result<String> = cityDetailsModel // invalid
val cityDetailsAny: Result<Any> = cityDetailsModel // valid

So yes, because Result is covariant in R (out R), you can only assign instances of Result<R> to variables of type Result<R or a supertype of R>.

I hope it's clearer now why saying T needs to be a subtype of R is not really correct and is a shortcut that may bring confusion, that's why I refrain from writing it this way.

Related