Author profile picture

In-Projection

An in-projection is a kind of type projection that makes a generic contravariant, but also causes any functions that return the type parameter to return it as a more abstract type.

Why is it called a projection? Because it’s a limited view of a real object. In regular life, a shadow is a kind of projection - it’s a limited two-dimensional representation of an actual three-dimensional thing. Just like a shadow lacks some qualities of the object that it’s created from, an in-projection lacks the normal return types of the type it’s projecting.

Example

To demonstrate in-projections, let’s create an extension function on a Dog class, which will allow us to add it to a Java Queue:

fun Dog.addToQueue(queue: Queue<in Dog>) = queue.add(this)

Notice the type of the parameter queue above - it’s not Queue<Dog>, but Queue<in Dog>. Because we added the in variance annotation to the type argument Dog, we’ve got an in-projection of a dog queue. That means that this function can accept a Queue<Dog>, as you’d expect, but it can also accept a Queue<Animal>!

val bruno = Dog("Bruno")

val dogQueue: Queue<Dog> = LinkedList()
bruno.addToQueue(dogQueue)

val animalQueue: Queue<Animal> = LinkedList()
bruno.addToQueue(animalQueue)

If you were to remove in from the type argument, this last line would no longer compile, because Queue<Animal> isn’t normally a subtype of Queue<Dog>.

The Limitation: in means “out” positions are crippled

In order to get this subtyping, we had to make a trade-off: any time we pull a pet off of the queue inside this function, we lose the type safety.

Let’s try it. Before we add our dog to the queue, we’ll take a peek to see who’s at the head of the queue.

fun Dog.addToQueue(queue: Queue<in Dog>) {
    val nextDog: Any? = queue.peek() // This returned `Any?` instead of `Dog`!
    queue.add(this)
}

This points out an important limitation with in-projections:

  • Calling peek() on a Queue<Dog> returns a Dog, but
  • Calling peek() on a Queue<in Dog> returns an Any?

This is the case for all functions and getters that put that type parameter in the “out” position (i.e., that return an object whose type is that type parameter).

Why is this the case?

Since our addToQueue() function uses an in-projection, it means it can accept a queue of Dog or anything higher on its type hierarchy. So, it’ll accept a Queue<Dog>, Queue<Animal>, or Queue<Any?>. For example:

val queue: Queue<Any?> = LinkedList()
queue.add(12345)
bruno.addToQueue(queue)

This queue has an integer in it! Since the queue can have anything at all in it, the safest type that we can assume when pulling an item out is Any?.

When to use in-projections

An in-projection can be helpful when:

  1. You want contravariance on a generic. For example, you want to accept Queue<Animal> in addition to Queue<Dog>.
  2. You don’t control that generic. Otherwise, Declaration-Site Variance might be a better way to go.
  3. You don’t need to get the item out of the generic, or if you do, you’re cool with it having type Any?. In-projections will only allow you to pull that type parameter out as Any?.

Share this article: