С нуля до распределенных приложений.
Создатель языка Скала, Мартин Одерски, был создателем обобщенных классов в языке Java. Не удивительно, что этот механизм усилен и улучшен в Скала.
Что такое обобщенные типы? Это обобщение функциональности типа относительно некоторого другого. Это очень абстрактное объяснение, которое проще пояснить на примере.
Допустим, мы моделируем некоторый интерпретатор и нам нужен класс объединяющий переменную с ее именем:
case class NamedInt(name: String, get: Int)
Если мы захотим создать классы “именованная строка” или “именованное число с плавающей точкой”, то нам придется создавать для этого отдельные классы NamedString и NamedDouble.
Для упрощения этого механизма используются обобщенные типы:
case class Named[T](name: String, get: T)
val namedDouble = Named("y", "1.5")
val namedString = Named("name", "some string")
Представим следующую иерархию классов животного мира:
abstract class Animal(val tongue: String) {
val age: Int
def isAlive: Boolean
}
case class Dog(age: Int) extends Animal("bark") {
def isAlive = age < 13
}
case class Cat(age: Int, diet: String) extends Animal("meow") {
def isAlive = age < 15
}
object ShroedingersCat extends Cat(0, "neutrino") {
val rng = new Random()
override def isAlive = rng.nextDouble() < 0.5
}
И функцию, которая заставляет именованное животное говорить (без насилия):
def speak(na: Named[Animal]): Unit = {
val animal = na.get
if (animal.isAlive)
println(s"${na.name}: ${animal.tongue}")
}
Код приведенный выше вызывает ошибку компиляции, которая гласит, что class Named is invariant in type T.
Это значит, что мы не можем передавать Named[Cat] в Named[Animal], т.к. класс Named не подчиняется иерархии наследования его аргументов, т.е. он инвариантен относительно своего аргумента.
В данном случае мы можем это исправить добавив один символ +:
case class Named[+T](name: String, get: T)
Теперь класс Named подчиняется той же иерархии наследования, что и его аргумент и Named[Cat] будет подклассом Named[Animal].
Есть много неочевидных случаев, когда делать тип ковариантным приводит к ошибкам. В частности, ковариантные типы должны быть неизменямыми. В языке Скала это предотвращается на этапе компиляции.
В C#, например, тип массива объектов ковариантен, т.е. Dog[ ] является производным от Animal[ ] и это ведет к возможности следующих ошибок:
void replaceCat(Animal[] cats, firstCat: Cat) {
animals[0] = cat;
}
Dog[] dogs = ...;
replaceCat(dogs); // dogs are animals
dogs[0].bark(); // BOOM! KotopesDoesntExistException
Это противоположность ковариантности, тип имеет иерархию наследования обратную его аргументам. Для чего это может быть нужно?
Введем класс ветеринара, который будет лечить наших животных.
class Vet[A]
def treatDogs(vet: Vet[Dog]) {}
val commonVet = new Vet[Animal]()
treatDogs(commonVet) // error: class Vet is not contravariant in type A
Казалось бы, животный ветеринар должен уметь лечить и собак, т.к. они подкласс всех животных, то есть нам нужна обратная иерархия наследования. Это достигается добавлением - к шаблонному аргументу относительно которого класс должен быть контравариантным:
class Vet[-A]
// ...
treatDogs(commonVet) // all fine
Класс может быть одновременно ковариантен к одним типам и контравариантен к другим.
abstract class Function[-T, +R] {
def apply(arg: T): R
}
Хорошей визуализацией понятий вариантности является следующая картинка: