Что такое variance (+A, -A) и зачем она нужна?

Вариантность (variance) — это концепция, описывающая, как параметризованные типы ведут себя при наследовании. В Scala она используется для управления совместимостью обобщённых типов (generic types), когда один тип является подтипом другого. Вариантность влияет на то, можно ли использовать, скажем, List[Dog] там, где ожидается List[Animal].

В Scala есть три вида вариантности:

1. Ковариантность (+A)

Ковариантность означает: если Dog — подтип Animal, то Box[Dog] — подтип Box[Animal].

Это полезно, когда параметр типа используется только для чтения, и экземпляр не должен модифицировать объект типа A.

Пример:

class Animal
class Dog extends Animal
class Cage\[+A\](val animal: A)
val dogCage: Cage\[Dog\] = new Cage(new Dog)
val animalCage: Cage\[Animal\] = dogCage // OK

Таким образом, ковариантность позволяет передавать Cage[Dog] туда, где ожидается Cage[Animal].

Важно: ковариантные типы не могут иметь var-поля с типом A, потому что это может привести к нарушению типобезопасности. Только val допустим.

2. Контрвариантность (-A)

Контрвариантность означает: если Dog — подтип Animal, то Handler[Animal] — подтип Handler[Dog].

Это используется, когда параметр типа применяется только для записи, например, в функциях, которые принимают значения, но не возвращают их.

Пример:

class Animal
class Dog extends Animal
class Handler\[-A\] {
def handle(animal: A): Unit = println(animal)
}
val animalHandler: Handler\[Animal\] = new Handler\[Animal\]
val dogHandler: Handler\[Dog\] = animalHandler // OK

Контрвариантность особенно полезна для функциональных аргументов. Например, обработчики событий, логеры, валидаторы и другие объекты, которые "потребляют" значения.

3. Инвариантность (без + или -)

Если параметр типа A указан без + или -, то тип Box[Dog] не совместим с Box[Animal], даже если Dog подтип Animal.

Пример:

class Box\[A\](val value: A)
val dogBox: Box\[Dog\] = new Box(new Dog)
val animalBox: Box\[Animal\] = dogBox // Ошибка компиляции

Это наиболее безопасное поведение, применяемое по умолчанию, чтобы избежать нежелательных последствий при комбинировании чтения и записи.

4. Почему это важно

Представим следующую ситуацию:

class Animal
class Dog extends Animal
class Cat extends Animal
class Cage\[+A\](val a: A)
def putCatInCage(cage: Cage\[Cat\]) = ???
val dogCage: Cage\[Dog\] = new Cage(new Dog)
val animalCage: Cage\[Animal\] = dogCage // OK, если +A
putCatInCage(animalCage) // Проблема!

Если Cage ковариантен, то Cage[Dog] можно подставить туда, где ожидается Cage[Animal], и тогда кто-то может положить туда Cat, а это нарушает ожидания.

Поэтому вариантность помогает сохранить типобезопасность при наследовании и обобщении.

5. Функции и вариантность

Функции в Scala — особый случай. Их сигнатура:

A => B
  • Аргумент A — контрвариантен (-A)

  • Результат B — ковариантен (+B)

Это логично: если функция может принимать Animal, то она точно справится с Dog, а если она возвращает Dog, это допустимо там, где ожидается Animal.

6. Как выбрать нужную вариантность при проектировании классов

  • Если ваш тип только читает значения параметра типа (например, коллекции, обработчики событий), скорее всего, подойдёт ковариантность +A.

  • Если ваш тип только пишет значения параметра (например, логгеры, буферы записи), вероятно, нужна контрвариантность -A.

  • Если ваш тип и читает, и пишет, либо изменяет значение параметра типа — используйте инвариантность (никакого + или -).

7. Ограничения

Scala позволяет использовать вариантность только в положениях, где она безопасна. Компилятор анализирует, где используется параметр типа — на стороне входа (контрвариантно) или выхода (ковариантно), и запрещает использование варианта в противоположной позиции.

Например:

class Box\[+A\](var value: A) // Ошибка: нельзя var с ковариантным типом

8. Вариантность в стандартной библиотеке

  • List[+A] — ковариантна

  • Function1[-A, +B] — контрвариантна по входу, ковариантна по выходу

  • Option[+A] — ковариантна

  • Array[A] — инвариантна, потому что позволяет изменять элементы

Таким образом, вариантность в Scala — это мощный инструмент типобезопасности и обобщённого проектирования. Понимание её принципов важно при работе с коллекциями, API-интерфейсами и функциями высшего порядка.