1. Previous: Chapter 18
  2. Next: Chapter 20
Kotlin: An Illustrated Guide • Chapter 19

Generic Variance

Chapter cover image

As we learned in Chapter 12, we can substitute a subtype anywhere that a supertype is declared.

side image for uml? List<Cow> MutableList<FarmAnimal> MutableList<Cow>

When it comes to generics, though, things don’t always work the way we might expect. While MutableList<Cow> is a subtype of List<Cow>, it’s not a subtype of MutableList<FarmAnimal>.

With a little bit of magic, though, we can finagle parameterized types into becoming the subtypes that we need them to be. To do that, we first need to learn about covariance and contravariance, so let’s jump in!

Covariance

A signed contract, indicating that a coin is accepted and a snack is returned

Parker works for the local Parks & Recreation Department. Since the playground is often full of parents with hungry children, he thought, “Let’s add a vending machine in the pavilion.” So he spoke with Vinnie, a guy who owns a nearby business that sets up and stocks vending machines.

“I need a vending machine where kids can put in a coin and get back a snack”, said Parker. Vinnie agreed, so they drew up a contract.

Once the paperwork was signed, Vinnie installed the new vending machine at the park, where customers could insert a coin and receive a random snack, like trail mix, a bag of gummy bears, or a candy bar. The next day, when Parker saw a kid insert a nickel and receive a bag of trail mix from the machine, he smiled to himself, knowing all was well.

Parker looking on as a child is smiling about his bag of trail mix.

A few weeks later, Parker saw Vinnie replacing the machine. Parker was a bit nervous about the change, so he spoke to him. “This new machine is still going to accept a coin and return a snack, right?”

“Yes”, began Vinnie, “Trail mix and gummy bears are in short supply, though, so this vending machine is only going to return a candy bar. That’s still okay, right?”

Parker thought about it, and finally replied, “As long as it accepts a coin and returns a snack. A candy bar is a kind of snack, so that should be fine.”

After the new machine was substituted for the old one, a child came up, inserted a coin, and received a candy bar. Parker smiled, glad that the new machine did what it was supposed to.

Parker relieved that the vending machine is still working. A child is happy with his candy bar.

A few weeks passed. One day, Vinnie was at it again, replacing the machine with yet another one. A few minutes after the installation was complete, an irate father complained to Parker, “What’s wrong with the new vending machine? My hungry kid put in a coin and instead of getting a snack, he got a toy action figure - he can’t eat a toy!

A father is angry at Parker as his son is trying to eat an action figure.

Parker quickly caught up with Vinnie again. When asked about the new machine, Vinnie replied, “Oh, I thought I’d switch out the old vending machine with a new one that returns either a snack or a toy.”

“No, no, no! The contract said the machine would accept a coin and return a snack. Toys are not snacks!” Realizing that this new machine didn’t do what Parker needed, Vinnie agreed to resolve the issue.

Covariance and Substitution

As we can see from this story, some substitutions work, but others don’t.

  • When Vinnie replaced the original vending machine with one that only returns candy bars, everything was fine, because candy bars are still a kind of snack that kids can eat.
  • However, when he replaced it with the machine that returns either a snack or a toy, he broke his contract - the machine no longer returned only snacks.
A review of the three vending machines, showing which ones worked and which one did not.

This brings up some important points for us to explore about substitution in Kotlin. To get started, let’s model the first vending machine that Vinnie provided - the one that accepted a coin and returned a snack.

open class VendingMachine {
    open fun purchase(money: Coin): Snack = randomSnack()
}

Although this chapter won’t include definitions for Snack and its related types, here’s a class diagram that shows them with their type hierarchies.

UML Class Diagram for Product, Snack, and Toy ActionFigure TrailMix GummyBears CandyBar Toy BouncyBall Sticker Product Snack

Now let’s model Vinnie’s second vending machine by extending the one above. This one looks almost identical to it, but instead of a random snack, it only ever returns a candy bar.

class CandyBarMachine : VendingMachine() {
    override fun purchase(money: Coin): Snack = CandyBar()
}

Any code that expects a VendingMachine can work with a CandyBarMachine, because it’s a subtype of VendingMachine - it still accepts a coin and returns a snack, just like VendingMachine does. Because of this, we can substitute a CandyBarMachine for a VendingMachine. In other words, we can assign a CandyBarMachine to a variable declared as a VendingMachine.

An instance of a subtype can be assigned to a variable declared as one of its supertypes. val machine: VendingMachine = CandyBarMachine () supertype subtype

Similarly, we can send it as an argument to a function that excepts a VendingMachine.

An instance of a subtype can be passed as an argument to a function that expects one of its supertypes. fun purchaseSnackFrom (machine: VendingMachine) = machine. purchase ( Dime ()) val snack = purchaseSnackFrom ( CandyBarMachine ()) supertype subtype

As we saw in the story, a CandyBar is a kind of Snack, so it’s also safe to declare that the CandyBarMachine only returns CandyBar objects. Let’s take the code from Listing 19.2 and update the return type.

class CandyBarMachine : VendingMachine() {
    override fun purchase(money: Coin): CandyBar = CandyBar()
}

As the story revealed, this only works one way. When Vinnie tried to replace the vending machine with one that returned a product that could be either a snack or a toy, he broke his contract. Kids expect to be able to eat anything that the vending machine provides, but toys aren’t edible!

The same principle holds true in Kotlin - a subclass can’t return a more general type. For example, if we create a ToyOrSnackMachine that tries to return any kind of Product, we’ll get a compile-time error.

class ToyOrSnackMachine : VendingMachine() {
    override fun purchase(money: Coin): Product = randomToyOrSnack()
}
Error

Most classes and interfaces associate with other types. Associated types can appear in a variety of places - as function parameter types, function return types, property types, and so on. For example, the VendingMachine class associates with two other types:

  • Coin - the type of the parameter in purchase()
  • Snack - the return type of purchase()
Class type - VendingMachine - and two associated types - Coin and Snack. open class VendingMachine { open fun purchase (money: Coin): Snack = randomSnack () } class type associated type associated type

The VendingMachine class can have supertypes and subtypes. Similarly, the associated types Coin and Snack can also have their own supertypes and subtypes.

The nature of the relationship between the type hierarchy of VendingMachine and the type hierarchy of its associated types can be described by variance. As we look at the code of VendingMachine and CandyBarMachine side by side, we can see that relationship emerge.

CandyBarMachine is a subtype of VendingMachine, and CandyBar is a subtype of Snack. open class VendingMachine { open fun purchase (money: Coin): Snack = } class CandyBarMachine : VendingMachine () { override fun purchase (money: Coin): CandyBar = } is a subtype of is a subtype of

Specifically, we can see that as we make a more specific VendingMachine, we can return a more specific Snack from the purchase() function. Because they become more specific together, this kind of variance is called covariance, where “co-” is a prefix that means “together”. When talking about variance, developers normally say it one of these ways:

  • “A type is covariant with regard to its return types”
  • “A type is covariant on its return types”

To sum up this section, just remember that within a subtype (e.g., CandyBarMachine) a function can return a more specific type (e.g., CandyBar) than declared in its supertype, but not a more general type.

The adventures of Parker and Vinnie continue, though! Brace yourself - Vinnie is about to try substituting a few more vending machines!

Contravariance

A few weeks later, Parker saw Vinnie replacing the machine yet again. “We’re upgrading our machines so that they accept both coins and bills,” he explained.

Parker thought about it. “Well, I guess that’s fine. As long as the machine still accepts a coin and returns a snack.” After the new machine was installed, a kid walked up, inserted a dime, and got a snack from the machine. “Great,” Parker thought to himself, “Everything is still good”.

Things continued well over the next few weeks. Even though this new vending machine had a bill acceptor, none of the kids ever used it, because they only ever had coins.

A few kids with their coins, lining up to use the new vending machine.

Wouldn’t you know it - a few weeks later, Parker saw Vinnie replacing the machine one more time. Parker shrugged it off and continued on his way.

Ten minutes later, though, a young girl was crying next to the snack machine. “What’s wrong?” he asked her.

A little girl crying next to the vending machine, and Parker is confused.

“I wanted a snack. I have a nickel, but this machine only accepts a dime!” Parker looked at the new vending machine, and sure enough, it only had a slot for dimes, but not any other kind of coin.

Parker complained to Vinnie again. “No, no, no! The contract said the machine would accept a coin and return a snack. A nickel is a kind of coin, so the machine still needs to accept it!” Embarrassed, Vinnie put back the previous vending machine.

Contravariance and Substitution

This story shows again that some substitutions work, and some don’t.

  • When Vinnie replaced the original vending machine with the one that accepted both coins and bills, everything still worked, because it still accepted coins. The kids only ever had coins on them, so they never actually used the bill acceptor, but there was no harm in the machine having one, as long as it could still accept coins.
  • However, when Vinnie replaced it with the machine that accepted only dimes, he broke his contract - the machine no longer accepted nickels, quarters, or any other kind of coin.
A review of the vending machines in this section, showing which one worked and which one did not.

This illustrates additional points about substitution. Let’s take a look at the original VendingMachine we created back in Listing 19.1.

open class VendingMachine {
    open fun purchase(money: Coin): Snack = randomSnack()
}

As with Snack, this chapter won’t include any class definitions for Coin and its type hierarchy, but here’s a UML class diagram showing how they relate.

UML Class Diagram for Money, Coin, and Bill. TenDollar FiveDollar OneDollar Quarter Dime Nickel Bill Coin Money

As we saw, it was safe for Vinnie to replace the coin-based vending machine with one that can accept either coins or bills. Likewise, it would be safe for Kotlin to allow a subtype to declare a more general parameter type.

But guess what? If we try changing the parameter type from Coin to Money, we’ll get a compiler error!

class AnyMoneyVendingMachine : VendingMachine() {
    override fun purchase(money: Money): Snack = randomSnack()
}
Error

Again, this would be perfectly safe for Kotlin’s type system to allow. So why does this cause a compiler error?

As you might recall, Kotlin allows us to overload a function - a class or interface can have multiple functions that have the same name, as long as their parameter types differ. In order to support this feature, Kotlin won’t allow us to change the type of a function parameter in subtypes, as we’re trying to do above.

One way to fix this is to simply remove the override keyword, which means we’ll be overloading the function instead of overriding it.

class AnyMoneyVendingMachine : VendingMachine() {
    fun purchase(money: Money): Snack = randomSnack()
}

Just keep in mind that when we do this, we end up with two purchase() functions, each with its own body - one in AnyMoneyVendingMachine and one in VendingMachine.

So Kotlin’s overloading feature is getting in the way. Overloading only applies to functions declared with the fun keyword, though, so to work around this, we can change purchase() from a function to a property that has a function type, like this.

open class VendingMachine {
    open val purchase: (Coin) -> Snack = { randomSnack() }
}

With this change, we can rewrite AnyMoneyVendingMachine to override that property.

class AnyMoneyVendingMachine : VendingMachine() {
    override val purchase: (Coin) -> Snack = { randomSnack() }
}

And finally, we can replace the Coin parameter type with Money.

class AnyMoneyVendingMachine : VendingMachine() {
    override val purchase: (Money) -> Snack = { randomSnack() }
}

With this, we can assign an instance of AnyMoneyVendingMachine to a variable whose type is VendingMachine. When assigned to a VendingMachine variable, it can only accept a coin. But when assigned to an AnyMoneyVendingMachine, it can accept either a coin or a bill.

val vendingMachine: VendingMachine = AnyMoneyVendingMachine()
val anyMoneyMachine: AnyMoneyVendingMachine = AnyMoneyVendingMachine()

val snack1: Snack = vendingMachine.purchase(Dime())
val snack2: Snack = anyMoneyMachine.purchase(Dime())
val snack3: Snack = anyMoneyMachine.purchase(OneDollarBill())

As we did earlier, we can put these two classes side by side and discover the nature of the variance between the class type and the return type of purchase().

Same kind of image as earlier open class VendingMachine { open val purchase : (Coin) -> Snack ... } class AnyMoneyVendingMachine : VendingMachine () { override val purchase : (Money) -> Snack = ... } is a subtype of is a subtype of

This time, we can see that as we make a more specific VendingMachine, we can accept a more general parameter type in purchase. The arrows above are pointing in opposite directions, so this kind of variance is called contravariance.

As you recall from the story, when Vinnie tried to replace the vending machine with one that accepted a more specific kind of coin, he broke his contract. Similarly, we can’t create a subclass that accepts a more specific type than its superclass did. For example, if we update purchase so that its parameter type is Dime, we’ll get a compiler error.

class AnyMoneyVendingMachine : VendingMachine() {
    override val purchase: (Dime) -> Snack = { randomSnack() }
}
Error

So, we learned from this story that a subtype can declare that it accepts a more general type, but not a more specific type.

Now that we understand variance - including covariance and contravariance - its time to revisit what we learned, and see how these concepts apply to generics.

What Makes a Subtype a Subtype?

Whenever Vinnie replaced one vending machine with another, everything was fine as long as the new machine did everything that the contract said it would. Specifically, the contract stated that the vending machine must accept a coin and return a snack. If the new machine did those things, then it was a suitable substitute. On the two occasions when it broke the contract, it was not a suitable substitute.

So what makes a subtype a subtype? The ability to substitute it for one of its supertypes. A subtype must fully support the contract of its supertype. Specifically, this means it must obey the following three rules:1

  1. The subtype must have all of the same public properties and functions as its supertype.
  2. Its function parameter types must be the same as or more general than the ones in its supertype.
  3. Its function return types must be the same as or more specific than the ones in its supertype.

We usually think of a subtype as a class that extends another class, an interface that extends another interface, or a class that implements an interface. In all three of these cases, Kotlin’s type system will ensure that the subclass follows the three rules above. However, when it comes to parameterized types - such as VendingMachine<Snack> and VendingMachine<CandyBar> - we can’t explicitly declare that one type is a subtype of another.

To help demonstrate this, let’s convert VendingMachine to a generic class. As a part of this change, we’ll add a snack constructor parameter, which is the snack that will get returned when the purchase() function is called.2

class VendingMachine<T : Snack>(private val snack: T) {
    fun purchase(money: Coin): T = snack
}

As we learned in the last chapter we can create parameterized types from this generic, such as VendingMachine<Snack> and VendingMachine<CandyBar>. Even though we can’t explicitly declare that VendingMachine<CandyBar> is a subtype of VendingMachine<Snack>, it would be perfectly safe for this to be the case, because it wouldn’t break the contract - all three rules above would be satisfied.

We can visualize this by imagining what the effective parameterized type would look like. In other words, let’s take the generic VendingMachine above, and everywhere that the type parameter appears, we’ll replace it with the type argument. Are all three rules satisfied?

Effective parameterized types for VendingMachine<Snack> and VendingMachine<CandyBar>. class VendingMachine<Snack>( ) { fun purchase (money: Coin): Snack } class VendingMachine<CandyBar>( ) { fun purchase (money: Coin): CandyBar } 2. same as or more general than 3. same as or more specific than 1. same functions/properties

So, VendingMachine<CandyBar> fully satisfies the contract of VendingMachine<Snack>, which means it’s safe for it to be one of its subtypes. This won’t just happen automatically, though. For example, if we try assigning it to a variable declared as VendingMachine<Snack>, we’ll get a compiler error.

val candyBarMachine: VendingMachine<CandyBar> = VendingMachine(CandyBar())
val vendingMachine: VendingMachine<Snack> = candyBarMachine
Error

So, VendingMachine<CandyBar> won’t be a subtype of VendingMachine<Snack> until we tell Kotlin our intent by doing one more thing.

Variance Modifiers

As we saw above, VendingMachine<CandyBar> fully satisfies the contract of VendingMachine<Snack>, so it should be possible for it to be a subtype of it. However, a type parameter can potentially be used in lots of places throughout the body of a generic type. It could be used as the return type of a function, as the parameter of a function, as the type of a property, and so on. Let’s consider what would happen if we were to add a refund() function to the VendingMachine interface.

class VendingMachine<T : Snack>(private val snack: T) {
    fun purchase(money: Coin): T = snack
    fun refund(snack: T): Coin = Dime()
}

In this case, the type parameter T appears both as a function result type and as a function parameter type. Would this version of VendingMachine<CandyBar> satisfy the contract of VendingMachine<Snack>? Let’s compare the effective parameterized types again. First, let’s look at the purchase() function.

Effective parameterized types for VendingMachine<Snack> and VendingMachine<CandyBar> show that the purchase() contract is satisfied. class VendingMachine<Snack>( ) { fun purchase (money: Coin): Snack fun refund (snack: Snack): Coin } class VendingMachine<CandyBar>( ) { fun purchase (money: Coin): CandyBar fun refund (snack: CandyBar): Coin } 2. same as or more general than 3. same as or more specific than 1. same functions/properties

As before, when it comes to the purchase() function, VendingMachine<CandyBar> satisfies the contract of VendingMachine<Snack>. So far so good. Now let’s look at the refund() function.

Effective parameterized types for VendingMachine<Snack> and VendingMachine<CandyBar> show that the refund() contract is not satisfied. class VendingMachine<Snack>( ) { fun purchase (money: Coin): Snack fun refund (snack: Snack): Coin } class VendingMachine<CandyBar>( ) { fun purchase (money: Coin): CandyBar fun refund (snack: CandyBar): Coin } 2. same as or more general than 3. same as or more specific than 1. same functions/properties

Yikes! Even though the return type of refund() satisfies the contract, the parameter type does not, because CandyBar is not the “same as or more general than” Snack - it’s more specific. So, if VendingMachine<T> includes the refund() function, then VendingMachine<CandyBar> cannot be a subtype of VendingMachine<Snack>.

If Kotlin is going to treat VendingMachine<CandyBar> as a subtype of VendingMachine<Snack>, we have to promise Kotlin that we won’t use this type parameter as a function parameter type, like we did in refund(). Instead, it can only appear in out-positions - in other words, as a function return type, or as the type of a read-only property.

To make this promise, we can add a variance modifier to the type parameter. Kotlin has two variance modifiers - let’s check them out.

The out modifier

The first variance modifier is named out, and it’s our way of telling Kotlin that this type parameter will only appear in an out-position. Let’s add the out modifier to type parameter T.

class VendingMachine<out T : Snack>(private val snack: T) {
    fun purchase(money: Coin): T = snack
}

This simple change is all that’s needed to get the code from Listing 19.14 to compile without errors - VendingMachine<CandyBar> is now a subtype of VendingMachine<Snack>!

val candyBarMachine: VendingMachine<CandyBar> = VendingMachine(CandyBar())
val vendingMachine: VendingMachine<Snack> = candyBarMachine

As we saw earlier in this chapter, a type is covariant with its associated function result types. So by ensuring that this type parameter is only used as a result type, we know that it’s safe for the VendingMachine type to be covariant with T.

Again, the out modifier is a promise to Kotlin that we will only use the type parameter in the out-position. If we try to use this same type parameter in an in-position - that is, as a function parameter type - then we’ll get a compiler error. To demonstrate this, we can pop in the refund() function from Listing 19.15, and watch as the compiler calls us out for breaking our promise.

class VendingMachine<out T : Snack>(private val snack: T) {
    fun purchase(money: Coin): T = snack
    fun refund(snack: T): Coin = Dime()
}
Error

So, by adding the out modifier to the type parameter, we’ve got the advantage that VendingMachine<CandyBar> is now a subtype of VendingMachine<Snack>, but we’ve got the disadvantage that we can no longer use T in an in-position.

The in modifier

As you probably guessed, Kotlin includes a complement to the out modifier, called in. But before we look at it, let’s shake things up in the VendingMachine class. Instead of a type parameter for its snack type, let’s use the type parameter for its money type.

class VendingMachine<T : Money> {
    fun purchase(money: T): Snack = randomSnack()
}

Similar to before, we can try to assign an instance of VendingMachine<Money> to VendingMachine<Coin>, but we’ll get an error.

val moneyVendingMachine: VendingMachine<Money> = VendingMachine()
val coinVendingMachine: VendingMachine<Coin> = moneyVendingMachine
Error

Even though T is only being used in the in-position, we have to declare this to Kotlin with the in variance modifier.

class VendingMachine<in T : Money> {
    fun purchase(money: T): Snack = randomSnack()
}

With this change, the code from Listing 19.20 successfully compiles.

val moneyVendingMachine: VendingMachine<Money> = VendingMachine()
val coinVendingMachine: VendingMachine<Coin> = moneyVendingMachine

As with the out modifier, we’ve made a trade-off here: when we declare a type parameter with the in modifier, we’re promising Kotlin that it will only ever appear in the in-position. Again, if we add a refund() function, we would need T in the out-position - as that function’s return type - which would cause a compiler error.

class VendingMachine<in T : Money>(private val money: T) {
    fun purchase(money: T): Snack = randomSnack()
    fun refund(snack: Snack): T = money
}
Error

In summary:

  • The out modifier can be used to ensure that the type parameter will only appear publicly in an out-position, which makes it safe for covariance.
  • Conversely, the in modifier can be used to ensure that it will only appear publicly in an in-position, so that it’s safe for contravariance.

In order to keep things simple, we’ve only used one type parameter at a time so far in this chapter. It’s entirely possible to have multiple type parameters, though, and when we do, we can use a variance modifier on each one. Let’s see how that looks.

Variance on Multiple Type Parameters

It’s important to note that generic variance doesn’t describe the type as a whole - it describes the relationship of the type to one of its type parameters. So, it can be covariant on one and contravariant on another. Instead of a single type parameter for either the Money or Product, let’s include one for each.

class VendingMachine<in T : Money, out R: Product>(private val product: R) {
    fun purchase(money: T): R = product
}

In this code, VendingMachine is contravariant with respect to T, and covariant with respect to R. From this generic class, we could instantiate a wide range of parameterized types. Some of them would include:

Lots of parameterized types created from the VendingMachine generic class. VendingMachine<Money, Product> VendingMachine<Coin, Product> VendingMachine<Nickel, Snack> VendingMachine<Nickel, Product> VendingMachine<Coin, Toy> VendingMachine<Money, ActionFigure>

With two type parameters, all those types of money, and all those types of product, it’s easy to get lost in which of the parameterized types are subtypes of the others. Just remember that a subtype must fully support the contract of its supertype. Here’s how the type hierarchy looks for the parameterized types above.

The same parameterized types, arranged into a UML class diagram. VendingMachine<Coin, Toy> VendingMachine<Nickel, Product> VendingMachine<Nickel, Snack> VendingMachine<Coin, Product> VendingMachine<Money, Product> VendingMachine<Money, ActionFigure>

It’s helpful to know that we can apply variance modifiers to multiple type parameters. For the rest of this chapter, though, we’ll go back to a single type parameter in order to keep the examples easy to follow.

We’ve seen how the in and out modifiers create variance, but we’ve also seen the trade-offs - type parameters declared with in can’t be used as a result type, and those declared with out can’t be used as a function parameter type. But sometimes, those trade-offs just won’t cut it. When that’s the case, we can still get some of the benefits of variance by using type projections.

Type Projections

So far, we’ve been able to get VendingMachine<CandyBar> to be a subtype of VendingMachine<Snack> by using variance modifiers on our type parameters. Let’s update the code from Listing 19.16 so that it works with any kind of Product and any kind of Money.

class VendingMachine<out T : Product>(private val product: T) {
    fun purchase(money: Money): T = product
}

In this code, we’ve got out on the type parameter again. Because we put this variance modifier where the type parameter is declared, this is called declaration-site variance. We can’t always use declaration-site variance, though. For example, what if we really, truly need that refund() function from Listing 19.18?

class VendingMachine<T : Product>(private val product: T) {
    fun purchase(money: Money): T = product
    fun refund(product: T): Money = Dime()
}

We can’t use the out variance modifier, because T is used in an in-position in refund(). And we can’t use the in modifier, because it’s used in an out-position in purchase(). Are we just out of luck, here? Does this mean that VendingMachine<CandyBar> will never be considered a subtype of VendingMachine<Snack>?

Thankfully, Kotlin provides a second option. Instead of using a variance modifier on a type parameter, we can use it on a type argument. To demonstrate this, let’s start with a function that accepts a VendingMachine<Snack>.

fun getSnackFrom(machine: VendingMachine<Snack>): Snack {
    return machine.purchase(Dime())
}

Since there are no variance modifiers on the type parameter in Listing 19.26, VendingMachine<CandyBar> is not a subtype of VendingMachine<Snack>, so we won’t be able to call the function with an instance of CandyBarMachine.

val candyBarMachine: VendingMachine<CandyBar> = VendingMachine(CandyBar())
getSnackFrom(candyBarMachine)
Error

Now let’s add an out modifier to the type argument in the getSnackFrom() function, like this.

fun getSnackFrom(machine: VendingMachine<out Snack>): Snack {
    return machine.purchase(Dime())
}

With this change, the code from Listing 19.28 compiles successfully!

val candyBarMachine: VendingMachine<CandyBar> = VendingMachine(CandyBar())
getSnackFrom(candyBarMachine)

When we put a variance modifier on a type argument rather than a type parameter, we create variance at the place in our code where we’re using the type (e.g., getSnackFrom()) instead of where we declared it (e.g., VendingMachine). For this reason, we call this use-site variance.

Just like declaration-site variance, use-site variance comes with trade-offs. As we’ll see in a moment, because the machine parameter has the out modifier, we won’t be able to call the refund() function inside the body of this function. Thankfully, the body of this function has no need for the refund() function, so this trade-off is entirely acceptable here!

The function body of getSnackFrom() cannot call refund(), but it also does not need to. fun getSnackFrom (machine: VendingMachine< out Snack>): Snack { return machine. purchase ( Dime ()) } This function body can't call refund(), but it also has no need to.

Keep in mind that declaration-site variance applies to the whole project, but use-site variance works only in a specific part of the project where we put the variance modifier on the type argument. In the example code above, it only works for the getSnackFrom() function. So, if other functions in our project still need to use the refund() function, that’s completely fine. In those places, VendingMachine<CandyBar> simply wouldn’t be able to be a subtype of VendingMachine<Snack>.

Out-Projections

It’s important to note that, within the body of this function, machine does not have the type VendingMachine<Snack>. It has the type VendingMachine<out Snack>, which is a type projection. Since this type argument has the out modifier on it, this particular kind of type projection is called an out-projection.

What exactly is a projection?

Beach ball casting its shadow.

Think of a projection like the shadow of a ball. If you shine a light on it, it casts a shadow onto the wall. The ball itself is a sphere that has three dimensions, but the ball’s shadow on the wall is a circle that has only two dimensions. The shadow still resembles the ball, but it’s missing its depth.3

Similarly, when we create a type projection, it’s kind of like we’re removing some of the “depth” of that object by limiting the types of its function inputs and outputs. For example, in the getSnackFrom() function above, we created a type projection from VendingMachine<Snack>, called VendingMachine<out Snack>, which looks a lot like the original, except that the refund() function no longer accepts a Snack. Instead, it accepts a type called Nothing!

The original generic class, the effective parameterized type, and the effective out-projected type. class VendingMachine< out Snack>( ) { fun purchase (money: Money): Snack = fun refund (product: Nothing): Money = } Effective Out-Projected Type class VendingMachine<Snack>( ) { fun purchase (money: Money): Snack = fun refund (product: Snack): Money = } Effective Parameterized Type class VendingMachine< T : Product>( ) { fun purchase (money: Money): T = fun refund (product: T ): Money = } Original Generic Class

What exactly is Nothing? As mentioned in Chapter 14, every type in Kotlin is a subtype of a class named Any. Similarly, every type in Kotlin is also a supertype of a class called Nothing.

UML - Any and Nothing Circle Boolean String Nothing Any

The Nothing class can never be instantiated, because it only has a private constructor. Since it’s impossible to create an instance of Nothing, we’ll never be able to call refund() here in this function.

So, an out-projection is created by adding the out modifier to a type argument. It looks similar to the original parameterized type, but everywhere that the type argument appears in an in-position, it is replaced with the Nothing type.

In-Projections

As you can probably guess, we can also use an in modifier to create an in-projection, like in this function.

fun getRefundFrom(machine: VendingMachine<in CandyBar>): Coin {
    return machine.refund(CandyBar())
}

As with the out-projection above, inside the body of this function, we end up with a projection of VendingMachine<CandyBar>. But this time, instead of affecting the functions’ parameter types, it’s the functions’ result types that have been affected.

With an in-projection, everywhere that the type argument appears in an out-position, it is forced to Any?. It’s entirely possible to call these functions and get a result. If we want to do anything useful with that result, though, we’ll probably need to cast it back to its more specific type.

The original generic class, the effective parameterized type, and the effective in-projected type. class VendingMachine< in Snack>( ) { fun purchase (money: Money): Any? = fun refund (product: Snack): Money = } Effective In-Projected Type class VendingMachine<Snack>( ) { fun purchase (money: Money): Snack = fun refund (product: Snack): Money = } Effective Parameterized Type class VendingMachine< T : Product>( ) { fun purchase (money: Money): T = fun refund (product: T ): Money = } Original Generic Class

So, out-projections and in-projections are two kinds of type projections. Out-projections create covariance by forcing the type argument in in-positions to Nothing, and in-projections create contravariance by forcing the type argument in out-positions to Any?.

Sometimes we want a function to accept all instances of a generic type, regardless of their type arguments. For these cases, Kotlin includes one more kind of projection.

Star Projections

Remember Vinnie? Well, at the end of the month, he services all of his vending machines, performing basic maintenance in order to keep them finely-tuned and working well. This operation doesn’t involve any Money or Product - he just needs to get in there and tighten a few bolts.

Here’s an updated version of VendingMachine that includes a function called tune(). As you can see, this new function doesn’t use a type parameter at all - neither as a function parameter nor as a return type.

class VendingMachine<T : Snack>(private val snack: T) {
    fun purchase(money: Coin): T = snack
    fun refund(snack: T): Coin = Dime()
    fun tune() = println("All tuned up!")
}

All vending machines need to be tuned up. The type of the Snack that they return is completely irrelevant.

In cases like these, we might want a function to accept literally any kind of type created from VendingMachine. Kotlin makes this easy with a special kind of type projection called a star-projection. To create a star-projection, rather than using a variance modifier, simply use an asterisk * (that is, a “star”) in place of the type argument. For example, here’s a function that will accept any kind of VendingMachine, regardless of its type argument.

fun service(machine: VendingMachine<*>) {
    print("Tuning up $machine... ")
    machine.tune()
}

This function can be called with any kind of VendingMachine, regardless of its type argument.

service(VendingMachine(CandyBar()) // Works with VendingMachine<CandyBar>
service(VendingMachine(TrailMix()) // Works with VendingMachine<TrailMix>
service(VendingMachine(GummyBears()) // Works with VendingMachine<GummyBears>

A star-projection looks like the original parameterized type, but:

  • Anywhere that the type parameter was used in an in-position, it’ll be replaced with the Nothing type.
  • Anywhere that the type parameter was used in an out-position, it’ll be replaced with the type parameter’s upper bound. Remember - if no upper bound was specified, it’ll be Any? by default.
The original generic class, the effective parameterized type, and the effective star-projected type. class VendingMachine( ) { fun purchase (money: Money): Product = fun refund (product: Nothing): Money = } Effective Star-Projected Type class VendingMachine<Snack>( ) { fun purchase (money: Money): Snack = fun refund (product: Snack): Money = } Effective Parameterized Type class VendingMachine< T : Product>( ) { fun purchase (money: Money): T = fun refund (product: T ): Money = } Original Generic Class

In summary, star-projections are useful when you want to accept any instance of a generic type, regardless of its type arguments.

Variance in the Standard Library

Now that we’ve learned all about covariance, contravariance, variance modifiers, and type projections, we can better understand why certain types in the standard library work the way that they do. We opened this chapter by presenting the fact that MutableList<Cow> is a subtype of List<Cow>, but it’s not a subtype of MutableList<FarmAnimal>. Why is this?

  • MutableList<Cow> is a subtype of List<Cow> because MutableList extends the List interface. That’s just regular interface inheritance.
  • MutableList<Cow> is not a subtype of MutableList<FarmAnimal> because it allows you to both read and modify its elements, which means its type parameter appears in both the in-position and out-position. As a result, it can’t have a variance modifier on it. This is similar to our VendingMachine in Listing 19.26.

As we’ve seen, the collection types in Kotlin usually come in two flavors - a read-only type (e.g., List) and a mutable type (e.g., MutableList). The read-only types will have the out modifier on their type parameters, so List<Cow> will be a subtype of List<FarmAnimal>. However, the mutable types won’t have any variance modifiers, so MutableList<Cow> will not be a subtype of MutableList<FarmAnimal>. As you now know, though, you can use a type projection to work around this when it makes sense!

Summary

Enjoying this book?
Pick up the Leanpub edition today!

Kotlin: An Illustrated Guide is now available on Leanpub See the book on Leanpub

As Parker sat on a bench, watching the families running around in the park, he thought about the differences in the vending machines that Vinnie had installed. Now that the two of them understood which vending machines would satisfy the contract, they both felt much more comfortable about any replacements they might need to make in the future. Vinnie walked up to the vending machine and inserted a dime. He turned to Parker, who was still sitting on the bench. “Want a snack?” he asked him.

Congratulations on working your way through this chapter! Here’s a recap of what we learned:

  • How covariance describes the relationship between a type and its function return types.
  • How contravariance describes the relationship between a type and its function parameter types.
  • How we can use variance modifiers on type parameters to create declaration-site variance.
  • How we can use variance modifiers on type arguments to create use-site variance.
  • How collection types in the standard library use variance modifiers.

Keep playing with these concepts in your Kotlin projects to help solidify your understanding! In the next chapter, we’ll introduce the very exciting topic of coroutines! See you then!


  1. The goal of this chapter is to explain generic variance, so these three rules are focused on the structure of the types. More strictly speaking, though, the behaviors of the classes should be compatible, as well. (See Liskov, B., & Wing, J. M. (1994). A behavioral notion of subtyping. ACM Transactions on Programming Languages and Systems, 16(6), 1811-1841). Also, Meilir Page-Jones observes these three rules more generally through the lens of preconditions, postconditions, and class invariants. (Page-Jones, M. (2000). Fundamentals of Object-Oriented Design in UML. Addison-Wesley Professional. p. 283). ↩︎

  2. Since the return type of purchase() varies depending on the type argument, we can’t just use randomSnack() any more - the snack has to have the same type as the type argument. For example, in VendingMachine<CandyBar>, the purchase() function must return a CandyBar, not a Snack. Setting this value in the constructor is a simple way to do this. If you prefer, you could change the snack property to a function type that constructs new snacks. e.g., private val snack: () -> T↩︎

  3. The word “projection” in the term “type projection” is technically rooted in mathematics, but the idea is the same. ↩︎

Share this article:

  • Share on Twitter
  • Share on Facebook
  • Share on Reddit