A type projection is a type that has been limited in certain ways in order to gain variance characteristics.
Imagine a three-dimensional object that you shine a flashlight onto. Behind that object on the wall is a two-dimensional projection of that object - a shadow. That projection has the same basic shape as the object, but only from one perspective.
Type projection is kind of like that - we take an existing object and reduce it to just the attributes or operations that we need at that place in the code. By reducing the object to a new form, you can allow subtyping that would not otherwise be possible.
How It Relates to Use-Site Variance
When you want subtyping to apply to your generic types (that’s what we call generic variance), Kotlin gives you two options:
- Declaration-site variance - you can specify the variance within the definition of the class or interface.
- Use-site variance - you can specify the variance at the places in your code where you’re using an instance of a generic.
Type projection is the result of use-site variance. When you apply use-site variance to an object, that object’s type is projected to a more limited view of the type.
Example
Here’s a class called Box
, which simply wraps any object.
class Box<T>(var item: T)
We say this class is invariant, because:
Box<Dog>
is not a subtype ofBox<Animal>
, andBox<Animal>
is not a subtype ofBox<Dog>
Now, it’s important to note that there are two ways that you can interact with an item
in this Box
:
- You can get the
item
out of theBox
- You can set the
item
in to theBox
However, at the location where we’re using Box
, it’s possible that we’re only ever going to interact with it in one of those ways. For example, you might only get the item out of it. We can communicate that intent to the compiler by adding the out
variance annotation, like this:
fun examine(boxed: Box<out Animal>) {
val animal: Animal = boxed.item
println(animal)
}
In this function, we’ve got a parameter called boxed
, whose type is not Box<Animal>
, but rather a projection of Box<Animal>
. Although you can normally call the setter on a Box<Animal>
, within the scope of the function, it’s not permitted. That brings us to the downside of projections…
The Downside - Restricting Functionality
In other words, the following function would not compile:
fun insert(boxed: Box<out Animal>) {
boxed.item = Animal() // <-- compiler error here
}
You’d get this compiler error:
Setter for ‘item’ is removed by type projection
Or if our setter were defined as an actual function with an argument, the equivalent compiler error would be something like this:
Out-projected type Box<out Animal> prohibits the use of ‘public final fun setItem(item: T): Unit’
Because we declared the type argument as out
, setters (or for that matter, any function that accepts that type parameter as an argument) are removed from the projection. We promised the compiler we won’t use the setter within this function.
The Upside - Subtyping
By creating a projection, we lose some functionality of that type, but hey - we traded it for the ability to add variance to the type, so that we can apply some subtyping! For example, our examine()
function above can accept either a Box<Animal>
or a Box<Dog>
.
val animal: Box<Animal> = Box(Animal())
val dog: Box<Dog> = Box(Dog())
examine(animal)
examine(dog)
This wouldn’t have been possible otherwise, because, as we noted above, the Box
class itself is invariant.
Kinds of Type Projection
There are three different kinds of type projection in Kotlin. Here’s a summary:
Type | Example | Variance | Restriction |
---|---|---|---|
Box<out Dog> |
Covariance | Type parameter cannot be used as a function argument or setter | |
Box<in Dog> |
Contravariance | If the type parameter is returned from a function or getter, its type will be Any? (or the upper-bound if you specified a type parameter constraint) |
|
Box<*> |
Every instance is a subtype | Both of the above restrictions apply |