Scala Tutorial

Basics

Control Statements

OOP Concepts

Parameterized - Type

Exceptions

Scala Annotation

Methods

String

Scala Packages

Scala Trait

Collections

Scala Options

Miscellaneous Topics

Scala | Variances

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:

  1. Covariant (+): If A is a subtype of B, then Container[A] is a subtype of Container[B].
  2. Contravariant (-): If A is a subtype of B, then Container[B] is a subtype of Container[A].
  3. Invariant: Container[A] and Container[B] are not related, regardless of the relationship between A and B.

Let's discuss each one with examples.

Covariant:

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]

Contravariant:

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]

Invariant:

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]

Why is Variance Important?

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.

  1. Benefits and drawbacks of covariance in Scala:

    • Description: Covariance allows subtyping relationships to be preserved in generic types. It enables more flexibility when working with collections. However, it may lead to unexpected behavior if not used carefully.
    • Code:
      class Container[+T](value: T)
      val container: Container[Animal] = new Container[Cat](new Cat)
      
  2. Contravariance and its use cases in Scala:

    • Description: Contravariance allows the reverse subtyping relationship. It is useful in scenarios where you want to generalize functions that consume a type.
    • Code:
      class Processor[-T] {
        def process(item: T): Unit = {
          // processing logic
        }
      }
      
  3. Invariance and immutability in Scala:

    • Description: Invariance ensures that the generic type must match exactly, preserving both subtyping and supertyping relationships. Immutability is often associated with invariance for consistent behavior.
    • Code:
      class Box[T](var content: T)
      val box: Box[Animal] = new Box[Animal](new Cat)
      
  4. Scala type system and variance annotations:

    • Description: Scala's type system incorporates variance annotations to define the relationships between generic types. The annotations include + for covariance, - for contravariance, and no annotation for invariance.
    • Code:
      class Box[+T](content: T)  // Covariant
      class Processor[-T]         // Contravariant
      class Queue[T]              // Invariant
      
  5. Variance annotations in Scala generics:

    • Description: Variance annotations can be applied to type parameters in class and trait definitions to specify the desired variance.
    • Code:
      trait Stack[+T] {
        def push[U >: T](item: U): Stack[U]
      }
      
  6. Practical examples of using variance in Scala:

    • Description: Variance is practical when working with hierarchical structures like collections or class hierarchies.
    • Code:
      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]