Scala Tutorial
Basics
Control Statements
OOP Concepts
Parameterized - Type
Exceptions
Scala Annotation
Methods
String
Scala Packages
Scala Trait
Collections
Scala Options
Miscellaneous Topics
In Scala, variance refers to how subtyping between more complex types relates to subtyping between their components. It's a key concept when working with generic types, especially collections. There are three kinds of variances:
+
): If A
is a subtype of B
, then Container[A]
is a subtype of Container[B]
.-
): If A
is a subtype of B
, then Container[B]
is a subtype of Container[A]
.Container[A]
and Container[B]
are not related, regardless of the relationship between A
and B
.Let's discuss each one with examples.
For example, if Dog
is a subtype of Animal
, then List[Dog]
is a subtype of List[Animal]
. In Scala, the List
type is covariant.
class Animal class Dog extends Animal def getAnimals: List[Animal] = List(new Dog, new Dog)
You can use the +
symbol in type parameters to denote covariance:
class CovariantContainer[+A]
This is the opposite of covariance. If Dog
is a subtype of Animal
, then Container[Animal]
is a subtype of Container[Dog]
. An example where this makes sense is for function arguments.
class Animal class Dog extends Animal abstract class AnimalPrinter[-A] { def print(animal: A): Unit } def printAnimals(printer: AnimalPrinter[Animal]): Unit = { printer.print(new Dog) } val dogPrinter: AnimalPrinter[Dog] = new AnimalPrinter[Dog] { def print(dog: Dog): Unit = println("It's a dog!") } printAnimals(dogPrinter)
You can use the -
symbol in type parameters to denote contravariance:
class ContravariantContainer[-A]
If a generic type is invariant in its type parameter, there's no subtyping relationship inferred based on the type argument. Most mutable collections in Scala are invariant because it's unsafe to have them covariant.
class InvariantContainer[A]
Consider a mutable container like an Array
in Scala. Suppose Array
were covariant. You'd then be able to do the following:
val dogs: Array[Dog] = Array(new Dog, new Dog) val animals: Array[Animal] = dogs animals(0) = new Animal // This line is problematic!
If Array
were covariant, the third line would be valid. But then on the fourth line, you'd be inserting an Animal
into what's supposed to be an array of Dogs
, leading to runtime errors. Therefore, Array
in Scala is invariant.
In conclusion, variance is a critical concept in type systems, especially in languages like Scala that support advanced type features. It helps ensure type safety while providing flexibility in how we work with generic types.
Benefits and drawbacks of covariance in Scala:
class Container[+T](value: T) val container: Container[Animal] = new Container[Cat](new Cat)
Contravariance and its use cases in Scala:
class Processor[-T] { def process(item: T): Unit = { // processing logic } }
Invariance and immutability in Scala:
class Box[T](var content: T) val box: Box[Animal] = new Box[Animal](new Cat)
Scala type system and variance annotations:
+
for covariance, -
for contravariance, and no annotation for invariance.class Box[+T](content: T) // Covariant class Processor[-T] // Contravariant class Queue[T] // Invariant
Variance annotations in Scala generics:
trait Stack[+T] { def push[U >: T](item: U): Stack[U] }
Practical examples of using variance in Scala:
trait Node[+T] case class Leaf[+T](value: T) extends Node[T] case class Branch[+T](left: Node[T], right: Node[T]) extends Node[T]