Variance is a crucial concept in Scala's type system. It determines how subtyping relationships between complex types relate to subtyping relationships between their component types. Understanding variance is essential for writing flexible and type-safe code in Scala.
Scala supports three types of variance:
Covariance is denoted by the +
symbol. It preserves the subtyping relationship of the type parameter.
class Box[+A](val content: A)
val intBox: Box[Int] = new Box(42)
val numberBox: Box[Number] = intBox // This is valid because Int is a subtype of Number
Covariance is useful when you want to use a more specific type in place of a more general type.
Contravariance is denoted by the -
symbol. It reverses the subtyping relationship of the type parameter.
trait Printer[-A] {
def print(value: A): Unit
}
def printNumber(printer: Printer[Number]): Unit = printer.print(42)
val intPrinter: Printer[Int] = new Printer[Int] { def print(value: Int) = println(value) }
printNumber(intPrinter) // This is valid because Printer[Int] is a subtype of Printer[Number]
Contravariance is useful for input types, such as function parameters.
Invariance is the default behavior in Scala. It means there's no subtyping relationship between complex types, regardless of their type parameters.
class Container[A](var content: A)
val intContainer: Container[Int] = new Container(42)
// val numberContainer: Container[Number] = intContainer // This would be invalid
Variance annotations can be applied to type parameters in class and trait definitions. They affect how instances of these types can be used in various contexts.
To deepen your understanding of Scala's type system, explore these related topics:
Mastering variance in Scala allows you to create more flexible and reusable code while maintaining type safety. Practice with different scenarios to fully grasp its implications and benefits in your Scala projects.