2

I understand covariance and contravariance. I also have read Eric Lippert's excellent post here.

However, I am failing to understand the practical application of variance for the following interfaces in the standard library which are required for writing custom delegated properties.

interface ReadOnlyProperty<in R, out T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
}

interface ReadWriteProperty<in R, T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
    operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}

Since property delegates are translated by the compiler, it is hard to find a practical example where variance comes into play. It is hard to wrap my head around, I am sure there is a reason.

Can someone help expound on this?

FreeGuwop
  • 23
  • 2
  • These are not required interfaces. They are just helpful to use because the signatures are kind of complicated to remember, and using them allows you to take advantage of IDE auto-completion. Not sure what you're asking about variance. Are you asking why they are marked `in R` and `out T` rather than just `R` and `T`? – Tenfour04 Apr 01 '20 at 19:03
  • I understand the same as you. I think that he/she is asking the reason behind making R and T covariant/contravariant by the case. – Dina Bogdan Apr 01 '20 at 19:13
  • @Tenfour04 Yes. For example, I can understand why a ReadOnlyProperty would be covariant (it only returns a value, does not accept one). However, I still do not understand when this would ever come into play. For the containing class, when would contravariance matter? – FreeGuwop Apr 01 '20 at 19:36

1 Answers1

1

Delegated property interfaces are not very useful for illustrating variance, because they are optional and you never actually directly call these functions yourself. And you're unlikely to build a class hierarchy with them or assign instances of them to variables.

The variance is not useful in this case, but rather simply suggests the compatibility.

In the case of delegates, thisRef has a contravariant type, indicating that a read-only delegate that is scoped to work within a certain class, is also allowed to be used within subclasses of that class.

Suppose you have a Cat class that meows and a delegate implementation that makes it meow whenever you read a delegated property:

open class Cat {
    fun meow() {
        println("meow")
    }
}

class MeowingDelegate<T>(private val value: T): ReadOnlyProperty<Cat, T> {
    override fun getValue(thisRef: Cat, property: KProperty<*>): T {
        thisRef.meow()
        return value
    }
}

You can use this delegate within a subclass of Cat because a subclass will have the methods of the super class, so the subclass also is also capable of meowing. The signature suggests that a MeowingDelegate with its in Cat type qualifies as a subclass of a delegate with a more specific type and can therefore be used in a Cat subclass.

open class Kitten: Cat() {
    val x: Int by MeowingDelegate(1)
}

Edit: For the covariant T for ReadOnlyProperty, it means that a delegate that provides one type of object can also be used as a provider of the supertype of that object. So if I had a delegate that provides Kittens, it could also be used to provide Cats:

open class Cat
open class Kitten: Cat() 

class KittenDelegate: ReadOnlyProperty<Any?, Kitten> {
    override fun getValue(thisRef: Any?, property: KProperty<*>): Kitten {
        return Kitten()
    }
}

class Sample {
    val cat: Cat by KittenDelegate()
}

Since the type T is invariant for the ReadWriteProperty, you can't do this with a Delegate that can only return Kittens. This makes sense, because the delegate also has to allow you to set the property, and so it wouldn't be able to accept any Cat and treat it as a Kitten.

Again, these delegate interfaces are not great examples for understanding variance since they are not typically used as interfaces. For a more practical example, you can look at familiar collection classes.

A read-only List has a covariant type. This allows you to easily cast collections to more specific types:

class Kennel (val cats: List<Cats>)

val kittens: List<Kitten> = listOf(Kitten(), Kitten())
val kennel = Kennel(kittens) // OK

You cannot do this with MutableList, since the type is invariant.

class Kennel (val cats: MutableList<Cats>)

val kittens: MutableList<Kitten> = mutableListOf(Kitten(), Kitten())
val kennel = Kennel(kittens) // Compiler error

But it's necessary for MutableList to be defined with an invariant type. A MutableList<Kitten> cannot accept any type of Cat to be added to it, so it wouldn't make sense to be able to cast it to a MutableList<Cat>. Conversely, a MutableList<Cat> does not necessarily have exclusively Kittens in it, so it doesn't make sense to be able to cast it to a MutableList<Kitten>. Of course, at the use site, you can give it variance, depending on your needs, and this is why a MutableList<Kitten> could be cast to a MutableList<out Cat>.

For class-level contravariance, I can't think of a common stdlib class that has it in the declaration. So suppose we create one that pets cats. Then we have some kittens that we want to pet. A CatPetter would be sufficient to use as a Petter<Kitten> because Kittens qualify as Cats and can therefore be pet by a CatPetter. If the type of Petter were invariant, this would not be allowed.

open class Cat {
    fun purr() {
        println("rrrr")
    }
}
open class Kitten: Cat()

interface Petter<in T: Cat> {
    fun pet(recipient: T)
}

class CatPetter: Petter<Cat> {
    override fun pet(recipient: Cat) {
        recipient.purr()
    }
}

class BoxOfKittens (val petter: Petter<Kitten>)

val x = BoxOfKittens(CatPetter()) // OK
Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • thisRef actually has a contravariant type (the R is denoted by in)...see https://github.com/JetBrains/kotlin/blob/24b8495e00c31dbafa9a418f357fd3168a64c57c/libraries/stdlib/src/kotlin/properties/Interfaces.kt#L19 I had the same thoughts as you but the situation is the opposite of this answer. – FreeGuwop Apr 01 '20 at 22:09
  • Oops, typo. Thanks. – Tenfour04 Apr 01 '20 at 22:16
  • I accepted your answer and gave an upvote (I have less than 15 reputation so it does not show). Can you also give an example of covariance for the ReadOnlyProperty? Thanks for the help, this is hard to visualize in my head. – FreeGuwop Apr 01 '20 at 22:35
  • OK, I wrote an update. I do think that these delegate interfaces are the least useful way to try to understand variance, since they aren't really used as interfaces. – Tenfour04 Apr 02 '20 at 14:47