Using values of projected types as arguments, for parameters of projected types

Hello, in order to understand variance better I have created this testing code:

val number: Number = 1f

class Box<T>(var item: T)

val numberBox: Box<Number> = Box(1f)
val outNumberBox: Box<out Number> = Box(2)
val inNumberBox: Box<in Number> = Box<Any?>("3")

fun <T : Number> numberBoxHandler(box: Box<T>, t: T) {}
fun <T : Number> outNumberBoxHandler(box: Box<out T>, t: T) {}
fun <T : Number> inNumberBoxHandler(box: Box<in T>, t: T) {}

Here are the testing functions:

//(test 1)
//OK
numberBoxHandler(numberBox, number)
  
//(test 2)
//ERROR at `number`: Type mismatch. Required: CapturedType(out Number) Found: Number
numberBoxHandler(outNumberBox, number)

//(test 3)
//ERROR at `inProjectedNumberBox`: Type mismatch. Required: Number Found: Number
//ERROR at `number`:               Type mismatch. Required: Number Found: Number
numberBoxHandler(inNumberBox, number)


//(test 4)
//OK
outNumberBoxHandler(numberBox, number)

//(test 5)
//OK
outNumberBoxHandler(outNumberBox, number)

//(test 6)
//ERROR at `inProjectedNumberBox`: Type mismatch. Required: Number Found: Number
//ERROR at `number`:               Type mismatch. Required: Number Found: Number
outNumberBoxHandler(inNumberBox, number)


//(test 7)
//OK
inNumberBoxHandler(numberBox, number)

//(test 8)
//ERROR at `number`: Type mismatch. Required: CapturedType(out Number) Found: Number
inNumberBoxHandler(outNumberBox, number)

//(test 9)
//OK
inNumberBoxHandler(inNumberBox, number)

 


I couldn’t make sense of the errors.

Type mismatch. Required: CapturedType(out Number); Found: Number

What is this CapturedType? It is not a function nor a class as far as I know.

Type mismatch. Required: Number; Found: Number

Why is it a mismatch?

Great examples!

First, you’re right that CapturedType() is not a function or a class. I’m not an expert, but I believe it is an internal of the compiler.

I’m not sure which examples you understand or not, so let’s go through all the ones that fail.

Test 2

First, notice that

val outNumberBox: Box<out Number> = Box(2)

is the same as

val intBox: Box<Int> = Box(2)
val outNumberBox: Box<out Number> = intBox

out Number does not mean “any type that subtypes Number”, it means “a specific type that subtypes Number, but I don’t necessarily know which one”.

This is why this example fails:

//ERROR at `number`: Type mismatch. Required: CapturedType(out Number) Found: Number
numberBoxHandler(outNumberBox, number)

Imagine if the function re-assigned Box.item: this would try to reassign outNumberBox, which is a Box<Int> with number, which is Float: of course, this is not safe.

Test 3

Sadly, the error message here is particularly unhelpful.

Let’s consider what would happen if it was allowed:

  • you create a Box("3") which is up-casted to Box<Any?> then Box<in Number>
  • you try to pass it to a function that accepts a type parameter, and two values that must conform to it.

It’s unclear to me what the compiler infers to be the type parameter here. To make it easier to analyse, let’s force it:

numberBoxHandler<Number>(inNumberBox, number)

Now, the compiler doesn’t complain about the number parameter. However, it does still complain about intNumberBox.

So what if it didn’t complain? Again, the numberBoxHandler could contain an assignment to Box.item, but inNumberBox is “a box of a specific supertype of Number”.

Compare how the compiler behaves:

Box<Any?>("3").item = "foo" // allowed
(Box<Any?>("3") as Box<in Number>).item = "foo" // forbidden, the compiler doesn't know if this is safe

In the first case, the compiler is sure that any type is allowed, so String must be allowed. In the second case, the compiler doesn’t know which type is allowed, only that whichever it is, it must be a supertype of Number.

Test 6

You can understand out as the modifier for values that an external actor can only read. Before looking at your example, notice what happens if you write:

outNumberBox.item = 2.0
// ERROR: Setter for 'item' is removed by type projection

By using out, we have effectively made the value read-only.

in is the opposite, it allows only setting the value. We can still read the value, but we will get a Any?, so we cannot do anything with it.

Now, let’s look at the example:

outNumberBoxHandler(inNumberBox, number)

We are saying that:

  • the function expects to be able to read the value of the first argument as a Number, but doesn’t care about setting it
  • the first parameter can only be read as Any?

As you can see, this is incompatible.

Note that the compiler is more paranoid about in than it is about out. There are multiple things that are mathematically safe with contravariance that the compiler refuses because it is considered too surprising. For example:

interface Foo {
    fun accept(a: Int): Number
}

// allowed, because function return types are 'out'
class Variance : Foo {
    override fun accept(a: Int): Int
}

// mathematically sound, because function parameters are 'in', 
// but still forbidden in Kotlin
class Contravariance : Foo {
    override fun accept(a: Number): Number
}

Test 8

I wish the compiler gave a way to display what type parameter it inferred. Anyway, here, the only type parameter you can use without changing the behavior is:

inNumberBoxHandler<Nothing>(outNumberBox, number)

I believe in this case the compiler inferred that the only possible type that could satisfy the first argument’s constraints is Box<Nothing>, because iit would be impossible to set it anyway, so all constraints on what values are used to set it are validated (through vacuous truth). However, if it is Nothing, then no value of that type exists—so the second argument doesn’t make sense.


Note that I’m not an expert on any of these questions, and there’s a chance I’m completely wrong about some of these explanations.

In practice, these complex cases are quite rare, because you either have mutable types, which are better left invariant, or read-only types, which you can safely use out with, without surprises. in is relatively rare to see.

2 Likes

Thanks for such a detailed response, I really appreciate your effort in going through all of the tests! :smiley:

Thanks to the answer I also realize that the tests are actually not good for demonstrating the behaviors.

For instance:

val int: Int = 1
val numberBox: Box<Number> = Box<Number>(number)
fun <T : Number> numberBoxHandler(box: Box<T>, t: T) {}

Here it might look as if the function is invoked with Int as the type argument for T:

numberBoxHandler(numberBox, int)

But the actual type argument is Number

numberBoxHandler<Number>(numberBox, int)

 

I will come back to this

This gets interesting, I found an example where providing explicit type argument is impossible!

class Box<T>(var item: T)
  
fun <T : Number> numberBoxConsumer(box: Box<T>) {}
  
val numberBox: Box<Number> = Box(1)
val outNumberBox: Box<out Number> = Box(2f)

numberBoxConsumer(numberBox)         //(1) OK
numberBoxConsumer<Number>(numberBox) //(2) OK, same as above but with explicit type argument

numberBoxConsumer(outNumberBox)      //(3) OK, but this is a bit starnge
numberBoxConsumer<X>(outNumberBox)   //(4) providing explicit type argument is impossible!

It is impossible to supply a type argument to (4) to make the code compilable!
Naturally the explicit type argument should be out Number, but type projection on function type arguments is forbidden so that cannot be done.
Similarly star projection is also forbidden because it is just a type of type projection.

If I had to try and explain the behavior of (3), perhaps it is because
of basic generics, where a single generic function:

fun <T : Number> numberBoxConsumer(box: Box<T>) {}

Can be “expanded” into multiple functions:

fun <Number> numberBoxConsumer(box: Box<Number>) {}
fun <Int> numberBoxConsumer(box: Box<Int>) {}
fun <Float> numberBoxConsumer(box: Box<Float>) {}
/* etc. */

And since out Number represents a range of types, i.e. Number and its sub types, Float, Int, etc., the compiler “auto-magically” resolved the types, and therefor (3) compiles.

Thoughts?

https://youtrack.jetbrains.com/issue/KT-63376/Type-mismatch-when-using-values-of-projected-types-as-arguments-for-parameters-of-projected-types#focus=Comments-27-8469217.0-0