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.