kaipa: (Default)
[personal profile] kaipa
Прошу прощения, если эта тема избита и изъезжена вдоль и поперек, но мне стоило немалого труда "переварить" в себе, зачем это нужно. Начну с конца, вернее, с середины. В Скале функциональный трейт 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. В заключение добавлю, что термины ковариантность и контрвариантность пришли в систему типов из теории категорий, глубокий смысл которой от меня пока ускользает.
This account has disabled anonymous posting.
If you don't have an account you can create one now.
HTML doesn't work in the subject.
More info about formatting

Profile

kaipa: (Default)
kaipa

April 2017

S M T W T F S
       1
2345678
9101112131415
16171819202122
23242526272829
30      

Style Credit

Expand Cut Tags

No cut tags
Page generated Mar. 25th, 2026 02:08 am
Powered by Dreamwidth Studios