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 aQueue<Dog>
returns aDog
, but - Calling
peek()
on aQueue<in Dog>
returns anAny?
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:
- You want contravariance on a generic. For example, you want to accept
Queue<Animal>
in addition toQueue<Dog>
. - You don’t control that generic. Otherwise, Declaration-Site Variance might be a better way to go.
- 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 asAny?
.