Home > front end >  subtypes/supertypes with upper bounded generics
subtypes/supertypes with upper bounded generics

Time:10-29

learning kotlin & have been reading about subtypes/supertypes and variance from https://typealias.com/guides/star-projections-and-how-they-work/ & generally, this website. I have a question that I don't think is covered (or maybe I'm just confused). Suppose you have a

open class A
open class B : A()

Pretty clearly A is a supertype of B. But what about the following?

open class Foo<T : A> {
fun doSomething(temp: T)
}

open class SubFoo : Foo<B>() {
}

Is SubFoo a subtype of Foo?

fun input(input: Foo<A>)
fun output(): SubFoo<B>

val inputParam = SubFoo()

input(inputParam) // works?

val ret: Foo<A> = output() // also works??

Intuitively I think the above works as desired, and the answer to the above question is yes. But I'm not completely sure, nor do I have a concrete explanation other than it resolves in my head. Honestly there's like 3 things going on here, the typing of A/B, the typing of Foo vs SubFoo, and upper bounding, and I think I'm getting lost in it all. Thanks in advance!!

CodePudding user response:

Is SubFoo a subtype of Foo?

No because Foo is not a type. Foo<A> and Foo<B> are types. Syntactically, Foo on its own is malformed unless the type parameter (the thing that goes in the <>) can be inferred.

In this case, SubFoo is a subtype of Foo<B> because it inherits from Foo<B>. SubFoo does not become a subtype of Foo<A> as a result of this though, so these do not work:

open class SubFoo : Foo<B>() {
    override doSomething(temp: B) {
       // do something that is specific to B
    }
}

fun input(input: Foo<A>) { }
fun output(): SubFoo = SubFoo()

val inputParam = SubFoo()

input(inputParam) // compiler error

val ret: Foo<A> = output() // compiler error

The idea that you also become the subtype of SomeGenericType<SuperType> by inheriting SomeGenericType<Subtype> is called covariance. You can make a type parameter of covariant by adding out to it. For example, List<T> is declared like this:

public interface List<out E>

So List<String> is a subtype of List<Any>.

However, this only works if it is safe to do so. In the case of Foo, it is not safe at all for its type parameter to be covariant. Consider what would happen if val ret: Foo<A> = output() were allowed. I could do:

open class C : A()

val ret: Foo<A> = output() // suppose this worked
ret.doSomething(C())

From the type checker's perspective, this looks all fine. ret is a Foo<A>, so its doSomething takes an A. C inherits from A, so it can be passed to an A parameter.

But what actually happens when this is run? A SubFoo is returned by output(), and SubFoo only accepts Bs in its doSomething method. Oopsies!

  • Related