Author profile picture

Type Projection

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.

UML diagram depicting subtyping of A and B. What is the subtyping for the MutableLists?

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:

  1. Declaration-site variance - you can specify the variance within the definition of the class or interface.
  2. 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 of Box<Animal>, and
  • Box<Animal> is not a subtype of Box<Dog>

Now, it’s important to note that there are two ways that you can interact with an item in this Box:

  1. You can get the item out of the Box
  2. You can set the item in to the Box

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
Out-Projected Box<out Dog> Covariance Type parameter cannot be used as a function argument or setter
In-Projected 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)
Star-Projected Box<*> Every instance is a subtype Both of the above restrictions apply

Share this article: