Abhijit Sarkar
Abhijit Sarkar

Reputation: 24548

Kotlin: generics and variance

I want to create an extension function on Throwable that, given a KClass, recursively searches for a root cause that matches the argument. The following is one attempt that works:

fun <T : Throwable> Throwable.getCauseIfAssignableFrom(e: KClass<T>): Throwable? = when {
    this::class.java.isAssignableFrom(e.java) -> this
    nonNull(this.cause) -> this.cause?.getCauseIfAssignableFrom(e)
    else -> null
}

This works too:

fun Throwable.getCauseIfAssignableFrom(e: KClass<out Throwable>): Throwable? = when {
    this::class.java.isAssignableFrom(e.java) -> this
    nonNull(this.cause) -> this.cause?.getCauseIfAssignableFrom(e)
    else -> null
}

I call the function like so: e.getCauseIfAssignableFrom(NoRemoteRepositoryException::class).

However, Kotlin docs about generics says:

This is called declaration-site variance: we can annotate the type parameter T of Source to make sure that it is only returned (produced) from members of Source, and never consumed. To do this we provide the out modifier

abstract class Source<out T> {
    abstract fun nextT(): T
}

fun demo(strs: Source<String>) {
    val objects: Source<Any> = strs // This is OK, since T is an out-parameter
    // ...
}

In my case, parameter e isn't returned, but consumed. It seems to me that it should be declared as e: KClass<in Throwable> but that doesn't compile. However, if I think of out as "you can only read from, or return, it" and in as "you can only write, or assign a value, to it", then it makes sense. Can someone explain?

Upvotes: 3

Views: 1353

Answers (3)

ephemient
ephemient

Reputation: 204778

The other answers have addressed why you don't need variance at this use site.

FYI, the API would be more usable if you cast the return to the expected type,

@Suppress("UNCHECKED_CAST")
fun <T : Any> Throwable.getCauseIfInstance(e: KClass<T>): T? = when {
    e.java.isAssignableFrom(javaClass) -> this as T
    else -> cause?.getCauseIfInstance(e)
}

but it's more Kotlin-like to use a reified type.

inline fun <reified T : Any> Throwable.getCauseIfInstance(): T? =
    generateSequence(this) { it.cause }.filterIsInstance<T>().firstOrNull()

This is effectively the same as writing an explicit loop, but shorter.

inline fun <reified T : Any> Throwable.getCauseIfInstance(): T? {
    var current = this
    while (true) {
        when (current) {
            is T -> return current
            else -> current = current.cause ?: return null
        }
    }
}

And unlike the original, this method does not require kotlin-reflect.

(I've also changed the behavior from isAssignableFrom to is (instanceof); I have a hard time imagining how the original could have been useful.)

Upvotes: 2

Les
Les

Reputation: 10605

In the example you cite from documentation, you are showing a generic class with the out annotation. This annotation give users of the class a guarantee that the class will not put out anything but a T or class derived from T.

In you code example, you are showing a generic function parameter with an out annotation on the parameter's type. This is giving users of the parameter the guarantee that the parameter will not be anything but a T (in your case a KClass<Throwable>) or a class derived from T (a KClass<{derived from Throwable}>).

Now reverse the thinking for in. If you were to use e: KClass<in Throwable> then you constrain the parameter to supers of Throwable.

In the case of your compiler error, it isn't the point whether or not your function uses methods or properties of e. In your case, the declaration of the parameter is constraining how the function can be called, not how the function itself uses the parameter. Therefore, using in instead of out disqualifies your call to the function with the parameter NorRemoteRepositoryException::class.

Of course, the constraints also apply within your function, but those constraints are never exercised because e is not used that way.

Upvotes: 1

hotkey
hotkey

Reputation: 147971

In your case, you don't actually use the variance of the type parameter: you never pass a value or use a value returned from a call to your e: KClass<T>.

The variance describes which values you can pass as an argument and what you can expect from the values returned from properties and functions when you work with the projected type (e.g. inside a function implementation). For example, where a KClass<T> would return a T (as written in the signature), a KClass<out SomeType> can return either SomeType or any of its subtypes. On contrary, where a KClass<T> would expect an argument of T, a KClass<in SomeType> expects some of the supertypes of SomeType (but it is unknown which exactly).

This, in fact, defines the limitations on the actual type arguments of the instances that you pass to such a function. With invariant type KClass<Base>, you cannot pass a KClass<Super> or KClass<Derived> (where Derived : Base : Super). But if a function expects KClass<out Base>, then you can also pass a KClass<Derived>, because it satisfies the aforementioned requirement: it returns Derived from its methods which should return Base or its subtype (but that's not true for KClass<Super>). And, on contrary, a function that expects KClass<in Base> can also receive KClass<Super>

So, when you rewrite getCauseIfAssignableFrom to accept a e: KClass<in Throwable>, you state that in the implementation you want to be able to pass a Throwable to some generic function or a property of e, and you need a KClass instance that is capable of handling that. The Any::class or Throwable::class would fit, but that's not what you need.

Since you don't call any of e's functions and don't access any of its properties, you could even make its type KClass<*> (clearly stating that you don't care what is the type and allow it to be anything), and it would work.

But your use case requires you to restrict the type to be a subtype of Throwable. This is where KClass<out Throwable> works: it restricts the type argument to be a subtype of Throwable (again, you state that, for the functions and properties of KClass<T> that return T or something with T like Function<T>, you want to use the return value as if T is a subtype of Throwable; though you don't do it).

The other option that works for you is defining an upper bound <T : Throwable>. This is similar to <out Throwable>, but it additionally captures the type argument of KClass<T> and allows you to use it somewhere else in the signature (in the return type or the types of the other parameters) or inside the implementation.

Upvotes: 1

Related Questions