Ковариантность, контрвариантность и границы типов в Scala

Oct 03, 2011 17:42

Прошу прощения, если эта тема избита и изъезжена вдоль и поперек, но мне стоило немалого труда "переварить" в себе, зачем это нужно. Начну с конца, вернее, с середины. В Скале функциональный трейт 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 System
Scala covariance/contrvariance at StackOverflow

P.S. В заключение добавлю, что термины ковариантность и контрвариантность пришли в систему типов из теории категорий, глубокий смысл которой от меня пока ускользает.

программирование

Previous post Next post
Up