I'm struggling with generics. In this distilled ex...
# getting-started
d
I'm struggling with generics. In this distilled example, I would like to make the signature
processBs
work, especially I would like to keep the
List<T>
return type. Is it possible? Code and playground link in thread.
The code
Copy code
sealed class A<T : A<T>>

class A1 : A<A1>()
class A2 : A<A2>()

sealed class B<T : A<T>> {
    abstract fun create(): T
}

class B1 : B<A1>() {
    override fun create() = A1()
}

class B2 : B<A2>() {
    override fun create() = A2()
}

fun <T : A<T>> processBs(bs: Set<B<T>>): List<T> =
    bs.map { it.create() }

fun main() {
    val bs = setOf(B1(), B2())
    processBs(bs)
}
s
Adding
out
variance to both generic types will work:
Copy code
sealed class A<out T: A<T>>
sealed class B<out T: A<T>>
j
Copy code
sealed class A<out T : A<T>>

sealed class B<out T : A<T>>
🫰 1
Sam was faster 😛
d
Currently, the compiler fails on the function invocation with
Copy code
Type mismatch.
Required:
A<Nothing>
Found:
A<*>
If I follow the IDE advice and change the signature to
Copy code
<T : A<T>> processBs(bs: Set<B<out A<*>>>): List<T>
then it fails on the function body with
Copy code
Type mismatch.
Required:
List<T>
Found:
List<A<*>>
Then I'm advised to change the signature again to
Copy code
fun <T : A<T>> processBs(bs: Set<B<out A<*>>>): List<A<*>>
and now it works but it violates my requirement that the function returns
List<T>
. Can make it work?
e
what do you think the current type of
bs
is?
because you wrote
B
as invariant, the common parent of
B1
and
B2
is
B<*>
d
Hm, actually it seems to be
B<out A<*>>
^
Anyway, the trouble is that the A classes are not under my control. They are actually Java classes, namely
GenericContainer
implementation, e.g.
public class KafkaContainer extends GenericContainer<KafkaContainer>
. What now? Am I screwed? 🙂
s
If you can’t change A, then change B to
Copy code
sealed class B<out T : A<out T>>
and change the
processBs
signature to
Copy code
fun <T : A<out T>> processBs(bs: Set<B<T>>): List<T> =
d
Hm, and if the T actually occurs on the in-position in the B's methods?
e
time for unchecked casting then
K 1
s
I mean… if it occurs in both in and out position then it’s invariant and your quest is impossible
d
Ok, I will investigate whether the unsafe casting actually makes sense in my situation. It might very well be that the problem is ill-defined.
On a more general note: What resources would you recommend to understand generics in detail? I've tried to read a few articles in the past and while I could understand their contents the topic never really clicked for me, i.e. I didn't develop the right intuition you guys seem to have.
s
https://kotlinlang.org/docs/generics.html is very good, to be honest. Just need to read it slowly and carefully.
👍 1
r
I found this article (and the articles following it) to be very helpful: https://typealias.com/guides/illustrated-guide-covariance-contravariance/
d
To iterate on yesterday's topic, I would appreciate design suggestions for my concrete situation because I think the actual semantics might indicate the correct solution. The code ATM looks like this:
Copy code
abstract class TestContainerRegistrar<T : GenericContainer<out T>> {
    protected abstract fun initializeContainer(environment: Environment): T

    protected open fun initRunningContainer(container: T, environment: Environment) {}

    protected open fun resetRunningContainer(container: T, environment: Environment) {}
}
To translate it to the naming in the original example,
GenericContainer
corresponds to class
A,
TestContainerRegistrar
to class
B
and the
B.create
method to
initializeContainer
. For illustration purposes, I've also included the other methods where the generic parameter is in the out position (therefore changing the class signature to
TestContainerRegistrar<out T : GenericContainer<out T>>
is not possible). One obvious solution would be to split the
TestContainerRegistrar
class into two, each containing just the covariant, resp. contravariant methods. But this doesn't make much sense to me. Any other ideas?
s
Can you have the
initializeContainer
function return an object which exposes both the container and its
init
and
reset
functions?
d
Yes, that would be an option. Thanks! Ideally, I would like to convert init/reset to extension functions on
GenericContainer
but AFAIK this can't be made polymorphic.