Contravariance

Understanding how contravariance affects the ability to use Generics in Kotlin.

fun main() {
    val roseTender = object: FlowerCare2<Rose2> {
        override fun prune(flower: Rose2) {
            println("I'm pruning a rose!")
        }
    }

    val daffodilTender = object: FlowerCare2<Daffodil2> {
        override fun prune(flower: Daffodil2) {
            println("I'm pruning a daffodil!")
        }
    }

    val roseGarden = Garden2(listOf(Rose2("Rosemary"), Rose2("Rosie")), roseTender)
    println(roseGarden.javaClass)

    val daffodilGarden = Garden2(listOf(Daffodil2(), Daffodil2(), Daffodil2()), daffodilTender)
    println(roseGarden.javaClass)

    roseGarden.tendFlower(0)

    daffodilGarden.tendFlower(2) // This is rather repetitive. We have to create Gardens and call tendFlower, but roseTender and daffodilTender are doing the same thing

    val flowerTender = object : FlowerCare2<Flower2> { // Create a generic flowerTender that can handle any type of Flower2
        override fun prune(flower: Flower2) {
            println("I'm tending a specifically-named ${flower.name}!")
        }
    }

    val roseGarden2 = Garden2(listOf(Rose2("Rosemary"), Rose2("Rosie")), flowerTender) // Won't work without adding 'in' to FlowerCare2 class, as expects FlowerCare2<Rose2>. We need <T> matching to be more relaxed.
    // Covariance preserves the subtyping. Contravariance is the opposite - starting with subclass and wanting to accept superclasses.
    // Comes at a price. We can only write them and not read from them. Cannot use them as return types from functions.

    roseGarden2.tendFlower(1)

}

class Garden2<T: Flower2>(val flowers: List<T>, val flowerCare: FlowerCare2<T>) {
    fun pickFlower(i: Int) = flowers[i]
    fun tendFlower(i: Int) {
        flowerCare.prune(flowers[i])
    }
}

open class Flower2(val name: String) {

}

class Rose2(val roseName: String): Flower2(roseName) {

}

class Daffodil2: Flower2("Daffodil") {

}

interface FlowerCare2<in T> { // Made contravariant by 'in' keyword. Will accept T or any superclass of.
  // As it is done in the declaration is known as 'declaration-site variance'. Java only has 'use-site variance' (see post of same name).
    fun prune(flower: T)
//    fun pick(): T // Cannot return this Generic as it is now contravariant
}

 

Covariance in Kotlin

Understanding how covariance affects the ability to use Generics in Kotlin.

fun main() {
    val shortList: List<Short> = listOf(1, 2, 3, 4, 5)
    convertToInt5(shortList) // All fine here

    val shortList2: MutableList<Short> = mutableListOf(1, 2, 3, 4, 5)
//    convertToInt6(shortList2) // Not allowed - type mismatch. But why? Surely Short is a sub-type (N.B. NOT a sub-class) of Number? And why did it work with an immutable List but not a mutable one?
    // List is a class, but List<String> is a type. Although we rationally would assume that it should accept a List<Short> if it accepts a List<Number>, it is not the same as a subclass.
    // So we want List<Short> to be a subtype of List<Number>. This is where the covariant keyword comes in - it preserves subtyping when working with Generics.
    // When looking at the Collections interface we can see that immutable Collections are covariant (<out E>) whereas mutable Collections are not - hence why the immutable List worked.

}

fun convertToInt5(collection: List<Number>) {
    for (num in collection) {
        println("${num.toInt()}")
    }
}

fun convertToInt6(collection: MutableList<Number>) {
    for (num in collection) {
        println("${num.toInt()}")
    }
}

fun tendGarden(roseGarden: Garden<Rose>) {
    waterGarden(roseGarden) // By default wants a garden<Flower> and we are passing it a Garden<Rose>, even though Rose is a subclass of Flower
    // This is because the Garden class is invariant, so will only accept Garden<Flower>. When we add 'out' to the Garden class this is resolved, but at a price:
    // We're now restricted and can only use the covariant class in the 'out' position, like an immutable Collection (can read but not add)
    // Function parameters are considered in the 'in' position (invariant) and the function return type is in the 'out' position (covariant)
    // Constructor parameters don't have 'in' or 'out' positions so you can always pass in a covariant class. No danger in this situation.
    // But if you pass in a var of type <T> to a constructor, it can't be covariant because setter needs to be generated. It would need to be invariant or declared as a val.
    // If you have a private function or property you don't need to worry about 'in' and 'out' - they're safe inside the class.
}

fun waterGarden(garden: Garden<Flower>) {

}

open class Flower {

}

class Rose: Flower() {

}

class Garden<out T: Flower> { // We can have a garden of daisies, daffodils etc. 'Out' keyword added to make it covariant.
  // As is done within the declaration is known as 'declaration-site variance'. Java only has 'use-site variance' (see post of same name).

    val flowers: List<T> = listOf()

    fun pickFlower(i: Int): T = flowers[i] // Pick the ith flower. Being overly verbose for readability
//    fun plantFlower(flower: T) {} // Not allowed as T is in 'out' position, not suitable as a parameter. This is to stop e.g. a daisy being planted in a rose garden.
    // This applies to member functions of the covariant class only.
}

// If you are ABSOLUTELY sure you will not pass an invalid type to your invariant class you can suppress the compiler warning by using the <@UnsafeVariance T> annotation.

 

Reified Parameters

Reify parameters to allow us to use Generics at runtime.

// Since Generics are erased at runtime then any Generic parameters that are passed to functions cannot determine which type it was invoked with
// Reification is ONLY useful when wanting to check the data type in a function
// Can't mark classes, properties or non-inline functions as reified
fun main() {

    val mixedList: List<Any> = listOf("string", 1, BigDecimal(22.5), "fall", BigDecimal(-349654.345))
    val bigDecimalsOnly = getElementsOfType2<BigDecimal>(mixedList) // Type in <> can be changed to whatever desired
    println(bigDecimalsOnly)
}

//fun <T> getElementsOfType(list: List<Any>): List<T> {
//    var newList: MutableList<T> = mutableListOf()
//    for (element in list) {
//        if (element is T) { // Will not work as T is erased at compile-time
//            newList.add(element)
//        }
//    }
//    return newList
//}

// This situation can benefit from inline functions as the parameters can be REIFIED - prevents the type from being erased at runtime:
inline fun <reified T> getElementsOfType2(list: List<Any>): List<T> { // Require both inline and reified declarations. By inlining the function the compiler can determine what type the parameter is and substitute it in at compile-time.
    var newList: MutableList<T> = mutableListOf()
    for (element in list) {
        if (element is T) { // Now works
            newList.add(element)
        }
    }
    return newList
}

 

Generics

Using generics.

How we would use data type specifications in Java and their benefits:

public class JavaGenerics {
    public static void main(String[] args) {

        List<String> list = new ArrayList<>(); // By giving an indication of data type we have improved error-checking, need less casting and code is clearer

        list.add("Hello");
//        list.add(new BigDecimal(10.5)) // Error-checking picks up on the incompatibility
        list.get(0).toUpperCase(); // IDE suggests functions related to data type

        List list1 = new ArrayList<>(); // However, we can still create a Collection without specifying type

    }
}

The same in Kotlin, introducing Generics:

// Use generics to give the compiler a hint as to what type of objects you are dealing with, often related to Collections
// Allows compiler to do some error-checking at compile time
fun main() {

    val list: MutableList<String> // Kotlin insists on knowing the data type
    list = mutableListOf("Hello")
    list.add("another string")
    printCollection(list)
    list[0].toUpperCase() // We can use the relevant functions

    println("======")

    val bdList = mutableListOf(BigDecimal(-33.45), BigDecimal(3503.99), BigDecimal(0.329))
//    printCollection(bdList) // Won't allow printCollection because it expects String
    printGenericCollection(bdList) // Works because Generics are used

    println("======")

    bdList.printCollectionExtension() // Same result using extension function - IDE suggests

// At runtime the application has none of the Generic information - type erasure. It is simply a syntactical feature e.g. it cannot run 'instanceOf' on a Generic since that information does not exist at runtime.
// Also true of Kotlin since it uses the JVM. How do we use the 'is' operator (equivalent of instanceOf)?
    if (list is List<String>){} // This is allowed! How come, since Generics are lost at runtime? Suspect the function is handled at compile-time by Kotlin
    val listAny: Any = listOf("str1", "str2") // To check this theory...
//    if (listAny is List<String>) {} // This is invalid as the statement cannot be evaluated at compile-time
//    if (listAny is List) // We can't even do this since Kotlin expects a data type
    if (listAny is List<*>) {// So we do this instead - star projection syntax, sort of like wildcard
        println("So let's assume this List contains strings") // If it isn't a List of non-Strings this
        val strList = listAny as List<String> // Casting. This is how we use 'as' in conjunction with this. If it isn't a List of Strings it will throw a ClassCastException. Compiler will warn
        println(strList[1].replace("str", "string"))
    }
}


fun printCollection(collection: List<String>) {
    for (item in collection) {
        println(item)
    }
}

fun <T> printGenericCollection(collection: List<T>) { // The <T> after fun is the type parameter declaration and is necessary
    for (item in collection) {
        println(item)
    }
}

// Extension function example:
fun <T> List<T>.printCollectionExtension() {
    for (item in this) {
        println(item)
    }
}