Generics upper bound type inference not working

Hey. I am trying to have this setup below where B is a subclass A:

(This will run in play.kotlinlang.org)

fun main() {
    val b = B(7L)
    val updatedB = doThing(b)
    doThingOnlyToB(updatedB)
}

fun <T : A> doThing(variable: T): T {
    return variable.copyA(id = 5L)
}

fun doThingOnlyToB(variable: B) {
    println("We did it! ${variable.id}")
}

abstract class A (
	open val id: Long
) {
    abstract fun <T : A>copyA(id: Long): T
}

data class B(
	override val id: Long
): A(id) {
    override fun <T : A>copyA(id: Long): T {
        return copy(id = id) // works if we add `as T` here
    }
}

The problem occurs in the override of copyA in the B subclass. Even though the default copy function that comes with the data class returns type B, the compiler gives me an error on that return line saying:

Type mismatch: inferred type is B but T was expected

I thought that since T has an upper bound of type A, and B is a subclass of type A, that returning type B from the overriden copyA function would be acceptable. If I add as T after using the data class copy function, then the compiler accepts it.

Does anyone know why this doesn’t work without the as T? Or if there is a better way?

The thing you have to understand about generic methods is that it is the caller of the method that picks the actual type argument. If you have a method with signature <T: A> copyA(Long) : T, then the signature implies that I as a caller can call pick any subtype of A, and copyA must return a value of that type. This includes any local class that I have defined myself, or even Nothing. I just have to write copyA<Nothing>(42). In general, it is almost always wrong to have a method with a type parameter, that is not used anywhere in the signature except as the return type.

What you can do instead is to simply make copyA non-generic and just have it return A in A. The overriding version in B can declare B as the return type.

Alternatively, you can make A itself generic, and let B specify itself as the type argument. It’s more complex, but in a few cases you can avoid some casting in certain cases. Most likely, you don’t need it, in which case I would recommend the simpler non-generic solution.

abstract class A<SELF: A<SELF>> {
    abstract fun copyA(id: Long) : SELF
}

data class B (val id: Long): A<B>() {
    override fun copyA(id: Long): B = copy(id)
}
2 Likes

Thank you @Varia! That made a lot of sense, and we are trying to use your first recommendation of not using generics and simply overriding the copyA in the subclass and return the subclass type instead of the parent type.

There is one more problem we are still struggling on. Below is the same code as above, but with the copyA function not using generics as per your recommendation. (again, this will run in play.kotlinlang.org)

fun main() {
    val b = B(7L)
    val updatedB = doThing(b)
    doThingOnlyToB(updatedB)
}

fun <T : A> doThing(variable: T): T {
    return variable.copyA(id = 5L) // Error on this line
}

fun doThingOnlyToB(variable: B) {
    println("We did it! ${variable.id}")
}

abstract class A (
	open val id: Long
) {
    abstract fun copyA(id: Long): A
}

data class B(
	override val id: Long
): A(id) {
    override fun copyA(id: Long): B {
        return copy(id = id)
    }
}

It does produce a similar error as before, but now in a new place, the doThing function:
Type mismatch: inferred type is A but T was expected

I think I understand the issue is that copyA returns type A which may or may not be the same type as the generic type T, even though T has an upper bound of type A.

What we are trying to achieve is that if the caller of doThing knows what subtype of A they have, that we can enforce the return type of doThing is also that same subtype of A. But if the caller of doThing only has a type of A and not a specific subtype, that the return type of doThing is simply A. All of that while calling the copyA function on the variable passed into doThing. Do you know of a way to achieve that?

That is exactly the kind of problem that can be solve using the second approach I suggested.

abstract class A<SELF: A<SELF>> {
    abstract fun copyA(id: Long) : SELF
}

data class B (val id: Long): A<B>() {
    override fun copyA(id: Long): B = copy(id)
}

fun <T : A<T>> doThing(variable: T): T {
    return variable.copyA(id = 5L)
}
2 Likes