pretty

Wednesday 24 February 2021

Kotlin contravariance samples

Contravariance is used to allow adding into collection values of type T and T subtypes. The collection itself, however, is allowed to contain items of type T and any super types of T (because of this it is not safe to get from this collection, with the exception of getting the root of type hierarchy Any?).

1. Use site variance

Sample: list with contravariance acts as a consumer.
It is safe to add into list the object of declared type B, or it's subtypes. It is not safe to retrieve object of type B from the list, because list of objects with super types of B may be provided. However, it is safe to retrieve the Kotlin most basic type Any? from the list.
 
open class A
open class B : A()
class C : B()

// Can receive list of B or its super types (A, Any, Any? types in this sample).
// Allows to put B or B subtypes into list (C in this sample).
// Cannot get fom List - except, can only get most generic type Any? from list.

fun listAsConsumer(list: MutableList<in B>) {
     // Type mismatch: inferred type is Any? but B was expected
     // val b: B = list[0] 
     
     // Type mismatch: inferred type is A but B was expected 
     // list.add(A())  
     
     // Cannot get fom List - except, can only get most generic type Any? from list.
     val any: Any? = list[0]
     
     // Allowed to add B or its subtypes into list
     list.add(B())
     list.add(C())
}

fun main() { 
    listAsConsumer(mutableListOf<A>(A())) 
} 

Sample: contravariance in Comparator.
Compare list of Ints using Comparator<Number>.
fun <T> Iterable<T>.sortedWithWrapper(comparator: Comparator<in T>): List<T> {
    return this.sortedWith(comparator)
}

fun main() { 
    val comparator: Comparator<Number> = Comparator { o1: Number, o2: Number ->
        o1.toDouble().compareTo(o2.toDouble())
    }
    val intList: List<Int> = listOf(4, 7)
    val result: List<Int> = intList.sortedWithWrapper(comparator)
}

The same sample without contravariance for Comparator is below. Without contravariance sortedWithWrapper() function could not return List<Int>, but only List<Number>. This happens because List<Int> is upcasted to List<Number>, to match the invariant T of the Comparator.
fun <T> Iterable<T>.sortedWithWrapper(comparator: Comparator<T>): List<T> {
    return this.sortedWith(comparator)
}

fun main() { 
    val comparator: Comparator<Number> = Comparator { o1: Number, o2: Number ->
        o1.toDouble().compareTo(o2.toDouble())
    }
    val intList: List<Int> = listOf(4, 7)
    
    // Type mismatch: inferred type is List<Number> but List<Int> was expected
    // val result: List<Int> = intList.sortedWithWrapper(comparator)
    val result: List<Number> = intList.sortedWithWrapper(comparator)
}


2. Declaration site variance

Declaration site variance is available in Kotlin, unlike Java where only use-site variance is possible.

Sample: Class with contravariance. Consumer cannot have contravariant type T in invariant or out positions.
open class A
open class B : A() 
class C : B() 
 
class Consumer<in T> {
    
    // Type parameter T is declared as 'in' but occurs in 'invariant' 
    // position in type T?
    // var x: T? = null

    fun consume(x: T) {}  
     
    // Ok, List is defined as List<out T>,
    // so it is a producer for this Consumer class
    fun consume(x: List<T>) {} 
     
    // Type parameter T is declared as 'in' but occurs in 'invariant' position 
    // in type Array<T>
    // fun consume(x: Array<T>) {} 
     
    // Type parameter T is declared as 'in' but occurs in 'out' position
    // in type T?
    // fun produce() : T? { return x }   
}

fun main() {   
    // Consumer of B (can be assigned Consumer of supertypes (A, Any, Any?)),
    // but now minimum B or its subtypes are accepted into consume()
    val consumerB: Consumer<B> = Consumer<A>() 
    consumerB.consume(C())
    
    // Type mismatch: inferred type is A but B was expected
    // consumerC.consume(A()) 
    
    // Type mismatch: inferred type is Consumer<B> but Consumer<A> was expected
    // val consumerA: Consumer<A> = Consumer<B>()  
}

Consumer class with contravariant T cannot accept another consumer with contravariant T.
class AnotherConsumer<in T> {
     fun consume() {}
}

class Consumer<in T> {
   
    // Type parameter T is declared as 'in' but occurs in 'out' position
    // in type AnotherConsumer
    // fun consume(x: AnotherConsumer<T>) {}  
}

This is due to AnotherConsumer can be extended by the class that cancels the contravariant 'in' modifier:
open class AnotherConsumer<in T> {
     open fun consume(x: T) {}
}

class AnotherConsumerChild: AnotherConsumer<T>() {
    var x: T? = null
    
    override fun consume(x: T) {
        this.x = x
    }  
    
    fun getT(): T? {
        return x
    }
}

Allowing to accept another contravariant consumer would lead to ClassCastException.
As an example of the above, in this snippet AnotherConsumerC would be called with the wrong type A (omitting compiler variance checking with @UnsafeVariance annotation).
open class A
open class B : A() 
class C : B() {
    fun c() {}
} 

open class AnotherConsumer<in T> {
     open fun consume(x: T) {}
}

// Overrides contravariance in parent type
class AnotherConsumerC: AnotherConsumer<C>() {
    override fun consume(x: C) {
        x.c()
    }  
}

open class Consumer<in T> {  
    open fun consume(anotherConsumer: AnotherConsumer<@UnsafeVariance T>) {}  
}  

open class ConsumerA: Consumer<A>() {  
    override fun consume(anotherConsumer: AnotherConsumer<A>) {
        anotherConsumer.consume(A())
    }  
}  

fun main() {   
    val consumerA = ConsumerA()
    
    // Allowed because parent Consumer<in T> is contravariant
    val consumerC: Consumer<C> = consumerA
    
    // Produces Exception in thread "main" 
    // java.lang.ClassCastException: A cannot be cast to C
    consumerC.consume(AnotherConsumerC())
}