Kotlin star projection on contravariant types

3.2k Views Asked by At

I am reading and trying to understand Kotlin type projections, sometimes I come up with confusing things like this:

For contravariant type parameters such as Consumer<in T>, a star projection is equivalent to <in Nothing>. In effect, you can’t call any methods that have T in the signature on such a star projection. If the type parameter is contravariant, it acts only as a consumer, and, as we discussed earlier, you don’t know exactly what it can consume. Therefore, you can’t give it anything to consume.

You can use the star projection syntax when the information about type arguments isn’t important: you don’t use any methods that refer to the type parameter in the signature, or you only read the data and you don’t care about its specific type. For instance, you can implement the printFirst function taking List<*> as a parameter.

What does it mean to contravariant type to have a star projection and how does it come up to

4

There are 4 best solutions below

0
On BEST ANSWER

This is also explained in the Kotlin documentation:

For Foo<in T>, where T is a contravariant type parameter, Foo<*> is equivalent to Foo<in Nothing>. It means there is nothing you can write to Foo<*> in a safe way when T is unknown.

Example

We have a class Foo<T> with contravariant T (declaration-site), i.e. Foo only works as a consumer of T:

class Foo<in T> {
    fun accept(t: T) {
        println(t)
    }
}

We can use this type in simple generic functions as follows:

fun <F> usingFoo(con: Foo<F>, t: F) {
    con.accept(t)
}

(used F in order to distinguish from class type parameter T)

This works fine since we use Foo as intended: As a consumer of Ts.

Now, your quote simply says that having a parameter of type con: Foo<*> instead of con: Foo<T> would have the following effect: "you can't call any methods that have T in the signature".

Thus, the following fails:

fun <F> usingFoo(con: Foo<*>, t: F) {
    con.accept(t) //not allowed!!
}

It's not safe to call accept with an instance of F because we don't know the type of Foo (denoted by star projection).

Star Projection: Sometimes you want to say that you know nothing about the type argument, but still want to use it in a safe way. The safe way here is to define such a projection of the generic type, that every concrete instantiation of that generic type would be a subtype of that projection.

0
On

Let's understand the reason why the star projection for contravariant Consumer<in T> is equivalent to <in Nothing>. But before that, we need to understand what exactly is a star projection and how it is for covariant Producer<out T>.


Star Projection

When you use a star projection of some generic class, you are not interested in using the functions or properties that return T or accept T as arguments from that generic class. For example, the following function just returns the bigger List of the two. We are only interested in the size property which doesn't return or accept T, it just returns an Int:

Example

fun getBiggerOfTwo(list1: MutableList<*>, list2: MutableList<*>) : MutableList<*> {
    return if (list1.size >= list2.size) list1 else list2
}

Why use * instead of specifying a type?

We want to keep the type unknown, so that we won't end up using the T specific functions accidently. For example, in the above function, if we are allowed to call the list1.add(Something()), we might end up mutating the list accidentally where we just intend to compare the lists. So, the compiler will help us by flagging an error when we call the add() function. Apart from creating the safety, our function will also be reusable for various types, not just some specific type.


Covariant without upper bound

In the following examples, we'll use the Producer and Consumer classes for producing and consuming various subtypes of class Pet(val cutenessIndex: String):

Declaration-site

class Producer<out T> {
    private val items = listOf<T>()
    fun produce() : T = items.last()
    fun size(): Int = items.size
}

Use-site

fun useProducer(star: Producer<*>) {
    // Produces Any?, a Pet is not guaranteed because T is unknown
    val anyNullable = star.produce()      // Not useful

    // Can't use functions and properties of Pet.
    anyNullable.cutenessIndex             // Error

    // Can use T-independent functions and properties
    star.size()                           // OK
}

Covariant with upper bound

Declaration-site

class Producer<out T : Pet> {
    private val pets = listOf<T>()
    fun produce() : T = pets.last()
}

Use-site

fun useProducer(star: Producer<*>) {
    // Even though we use * here, T is known to be at least a Pet
    // because it's an upper bound at the declaration site.
    // So, Pet is guaranteed.
    val pet = star.produce()              // OK

    // Can use properties and functions of Pet.
    pet.cutenessIndex                     // OK
}

Contravariant without lower bound

Declaration-site

class Consumer<in T> {
    private val items = mutableListOf<T>()
    fun consume(item: T) = items.add(item)
    fun size(): Int = items.size
}

Use-site

fun useConsumer(consumer: Consumer<*>) {
    // Cannot consume anything because 
    // lower bound is not supported in Kotlin and T is unknown.
    consumer.consume(Pet())               // Error

    // Can use T-independent functions and properties.
    consumer.size()                       // OK
}

Contravariant with lower bound

The lower bound is not supported in Kotlin. So, in the Consumer class above, we cannot have something like in Pet : T(lower bound) like we have out T : Pet(upper bound). As we know a consumer can consume T and it's subtypes. Nothing is the subtype of all types in Kotlin, just like Any? is the supertype of all types. And since, the T is unknown in the star projection, the only known subtype of T is Nothing. This is why a consumer's star projection can only consume Nothing. Hence, saying Consumer<*> is the same thing as saying Consumer<in Nothing>.


That's it! Hope that helps clearing things up.

0
On

To add to sm1..'s superb answer, here is an example for covariant types:

   class Foo<out U: Number> {
        private var u: U? = null
            
        fun produce(): U? {
            return u
        }
    }
    
    fun usingFoo(foo: Foo<*>) {
        foo.produce()    
    }
    
    fun main() {    
        usingFoo(Foo<Number>()) //OK
        usingFoo(Foo<Any>()) // Not OK
        
//Direct calls (not using method with *)    
    Foo<Number>().produce() //OK
    Foo<Any>().produce() //Not OK  
       
    }

Same behaviour in direct calls without star projections, but the * basically says: we defined a generic type (Foo), and at the point of use in a method (usingFoo), we still don't know the concrete type. The original constraint of super type (in this case, Number), remains. The * just says: the concrete type is defined elsewhere.

0
On

Further to answer above by user s1m..

class Foo<in T> {
    fun accept(t: T) {
        println(t)
    }

    fun doSomething(): String {
        return "yay"
    }
}

fun <F> usingFoo(con: Foo<*>, t: F) {
    //  con.accept(t) //not allowed!!
    val result = con.doSomething() // OK!
}

fun main() {
    val number: Int = 5
    usingFoo(Foo<Number>(), number)
} 

In terms of using the

Foo<*> 

in a safe way, the doSomething() method shows one example (the forbidden methods are those that consume type T).