Прошу прощения, если эта тема избита и изъезжена вдоль и поперек, но мне стоило немалого труда "переварить" в себе, зачем это нужно. Начну с конца, вернее, с середины. В Скале функциональный трейт Function1 -- контрвариантный по аргументу, и ковариантный по результату.
trait Function1[-P, +R] {
def apply(p: P): R
}
Это не какая-то экзотика, любая параметризированная функция от одного переменного в Скале ведет себя именно так. Что это значит? На уровне "наивных" определений, ковариантность, обозначаемая модификатором "+", -- это естественное движение "вниз" по иерархии типов, от более общих к более частным, а контрвариантность, обозначаемая модификатором "-", -- наоборот, вверх, от производных типов к базовым. Вроде бы все просто, но не совсем.
Сами параметры или аргументы функций -- естественно, ковариантны. Т.е. F(p: T) можно вызвать для любого p, производного от T типа. Однако сам функциональный тип ведет себя совершенно по-другому, проще всего это продемонстрировать таким выражением:
val f: Function1[P, R] = new Function1[Psup, Rsub] { … }, где Psup -- супер-тип от P, а Rsub -- производный тип от R.
Если подумать, то это выглядит довольно странно. Вот у нас есть функция из P в R, и ее частным случаем является функция из типа, более общего, чем P, в менее общий, чем R. Тем не менее, все очень рационально. Сначала, я поймал понимание, почему это так, практически сразу, но потом стал пытаться объяснить самому себе в деталях и запутался. Помог вернуть голову на место пример из документации. Тем не менее, я предложу простое аналитическое обоснование, изначально пришедшее мне в голову (его можно представить и геометрически).
Пусть F: P → R, т.е. p ∈ P => F(p) ∈ R
Рассмотрим, F' : P` → R' : P` ⊃ P, R` ⊂ R.
Отсюда, p ∈ P => p ∈ P` => F`(p) ∈ R`=> F`(p) ∈ R.
То есть F` -- частный случай F на ее области определения.
Возвращаясь к программированию, если у нас есть клиент, который ожидает функцию, определенную для некоторых типов, то вместо нее можно использовать функцию для более общих типов, так как клиент про общие типы ничего не знает, и все равно вызывает эту функцию в рамках частных типов.
Контрвариантность функциональных параметров имеет один занимательный сайд-эффект: если необходимо разработать естественный, то есть ковариантный, параметризированный тип, то его методы оказываются нерабочими. Классический пример:
trait List[+A] {
def cons(hd: A): List[A]
}
То есть у нас совершенно естественное желание, чтобы List[String] был подтипом List[Object], и поэтому используем ковариантный модификатор '+'. Однако, параметризированная функция cons содержит тип A в контрвариантной позиции, и компилятор выдаст ошибку. Что делать? Для этого в Скале есть трюк, позволяющий определять границы типов, а именно:
trait List[+A] {
def cons[B >: A](v: B): List[B]
}
Это означает, что метод cons определен для некоторого типа B, который является супер-типом от А, или A -- нижняя граница или lower bound для типа B. В частном случае, B может совпадать с A, но в общем случае функция cons может вернуть более общий тип B, что удовлетворяет естественному ощущению, что если в один список добавлять числа и строки, то получится список из более общих объектов.
Аналогичным образом в некоторых случаях целесообразно определять верхнюю границу или upper bound параметризированного типа.
Несколько англоязычных ссылок:
Scala Type SystemScala covariance/contrvariance at StackOverflow P.S. В заключение добавлю, что термины ковариантность и контрвариантность пришли в систему типов из теории категорий, глубокий смысл которой от меня пока ускользает.