Дженерики инвариантно-ковариантно-контравариантные в скале - PullRequest
0 голосов
/ 12 мая 2018

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

Я просматриваю страницу обобщений scala: https://docs.scala -lang.org / tour / generic-classes.html

Здесь сказано, что

Примечание: подтип универсальных типов инвариант . Это означает, что если мы иметь стек символов типа Stack [Char], тогда его нельзя использовать как целочисленный стек типа Stack [Int]. Это было бы неправильно, потому что это позволило бы нам ввести истинные целые числа в стек символов. к В заключение, стек [A] является только подтипом стека [B] тогда и только тогда, когда B = A.

Я полностью понимаю, что не могу использовать Char, где требуется Int. Но мой класс Stack принимает только тип A (то есть invariant). Если я добавлю в них яблоко, банан или фрукты, они все будут приняты.

class Fruit

class Apple extends Fruit

class Banana extends Fruit

  val stack2 = new Stack[Fruit]
  stack2.push(new Fruit)
  stack2.push(new Banana)
  stack2.push(new Apple)

Но на следующей странице (https://docs.scala -lang.org / tour / variances.html ) говорится, что параметр типа должен быть ковариантным +A, тогда как работает пример Fruit поскольку даже это добавляет подтипы с invariant.

Надеюсь, мне понятен мой вопрос. Дайте мне знать, если больше информации. необходимо добавить.

Ответы [ 3 ]

0 голосов
/ 12 мая 2018

Это никак не связано с дисперсией.

Вы объявляете stack2 как Stack[Fruit], другими словами, вы заявляете, что вам разрешено помещать что-либо в Stack, который является Fruit. Apple - это (подтип) Fruit, поэтому вы можете поместить Apple в Stack из Fruits.

Это называется подтипом и не имеет ничего общего с дисперсией .

Давайте сделаем шаг назад: что на самом деле означает дисперсия?

Ну, дисперсия означает «изменить» (думать о таких словах, как «изменить» или «переменная»). co- означает «вместе» (думать о сотрудничестве, совместном обучении, совместном размещении), contra- означает «против» (думать о противоречии, контрразведке, противодействии повстанцам) , контрацептив), а in- означает «не связанный» или «не-» (думать о недобровольном, недоступном, непереносимом).

Итак, у нас есть «изменение», и это изменение может быть «вместе», «против» или «не связано». Что ж, для того, чтобы иметь связанные изменения, нам нужны две вещи, которые меняются, и они могут либо изменяться вместе (то есть, когда одна вещь изменяется, другая вещь также изменяется «в том же направлении»), они могут изменяются друг против друга (то есть, когда одна вещь изменяется, другая изменяется «в противоположном направлении»), или они могут быть не связаны (то есть, когда одна вещь изменяется, другая нет.)

И это все, что есть в математической концепции ковариации, контравариантности и инвариантности. Все, что нам нужно, - это две «вещи», некоторое понятие «изменения», и это изменение должно иметь некоторое понятие «направления».

Теперь это, конечно, очень абстрактно. В этом конкретном случае речь идет о контексте подтипирования и параметрического полиморфизма. Как это применимо здесь?

Ну, каковы наши две вещи? Когда у нас есть конструктор типа , такой как C[A], тогда две наши вещи:

  1. Аргумент типа A.
  2. сконструированный тип , который является результатом применения конструктора типа C к A.

И каковы наши изменения с чувством направления? Это подтип !

Итак, теперь возникает вопрос: «Когда я изменяю A на B (вдоль одного из направлений подтипирования, т. Е. Делаю его подтипом или супертипом), то как C[A] относится к C[B]».

И снова есть три возможности:

  • Ковариация : A <: BC[A] <: C[B]: когда A является подтипом B, тогда C[A] является подтипом C[B], в других словами, когда я изменяю A по иерархии подтипов, то C[A] меняется с A в в том же направлении .
  • Контравариантность : A <: BC[A] :> C[B]: если A является подтипом B, то C[A] является супертипом C[B], другими словами, когда я изменяю A по иерархии подтипов, тогда C[A] меняет против A в противоположном направлении .
  • Инвариантность : между C[A] и C[B] нет отношения подтипов, ни подтип, ни супертип другого.

Есть два вопроса, которые вы могли бы задать себе сейчас:

  1. Почему это полезно?
  2. Какой из них правильный?

Это полезно по той же причине, что и подтипирование полезно. На самом деле, это просто подтип. Итак, если у вас есть язык, который имеет как подтип, так и параметрический полиморфизм, тогда важно знать, является ли один тип подтипом другого типа, и дисперсия говорит вам, является ли составной тип подтипом другого составного типа тот же конструктор, основанный на связи подтипов между аргументами типа.

Какой из них правильный, тем сложнее, но, к счастью, у нас есть мощный инструмент для анализа, когда подтип является подтипом другого типа: Принцип замещения Барбары Лисков говорит нам, что тип Sподтип типа T IFF любой экземпляр T может быть заменен экземпляром S без изменения наблюдаемых желаемых свойств программы.

Давайте рассмотрим простойуниверсальный тип, функция.Функция имеет два параметра типа, один для ввода и один для вывода.(Здесь мы все упрощаем.) F[A, B] - это функция, которая принимает аргумент типа A и возвращает результат типа B.

И теперь мы разыгрываем несколько сценариев,У меня есть какая-то операция O , которая хочет работать с функцией от Fruit с до Mammal с (да, я знаю, интересные оригинальные примеры!) LSP говорит, что я также должен быть в состоянии передатьв подтипе этой функции, и все должно работать.Допустим, F были ковариантными в A.Тогда я смогу передать функцию от Apple с до Mammal с.Но что происходит, когда O передает Orange на F?Это должно быть разрешено! O удалось передать Orange в F[Fruit, Mammal], потому что Orange является подтипом Fruit.Но функция из Apple s не знает, как обращаться с Orange s, поэтому она взрывается.LSP говорит, что это должно работать, но это означает, что единственный вывод, который мы можем сделать, состоит в том, что наше предположение неверно: F[Apple, Mammal] не является подтипом F[Fruit, Mammal], другими словами, F не является ковариантным в A.

Что если бы это было противоречиво?Что если мы передадим F[Food, Mammal] в O ?Ну, O снова пытается передать Orange, и это работает: Orange - это Food, поэтому F[Food, Mammal] знает, как обращаться с Orange s.Теперь мы можем заключить, что функции являются контравариантными на своих входах, то есть вы можете передать функцию, которая принимает более общий тип, в качестве входа для замены функции, которая принимает более ограниченный тип, и все будет работатьхорошо.

Теперь давайте посмотрим на вывод F.Что бы произошло, если бы F были контравариантны в B, как это было в A?Мы передаем F[Fruit, Animal] O .Согласно LSP, если мы правы и функции являются противоположными по своему результату, ничего плохого не должно произойти.К сожалению, O вызывает метод getMilk для результата F, но F только что вернул его Chicken.К сожалению.Следовательно, функции не могут быть противоположными в своих выходах.

OTOH, что произойдет, если мы передадим F[Fruit, Cow]?Все еще работает! O вызывает getMilk на возвращенной корове, и это действительно дает молоко.Таким образом, похоже, что функции являются ковариантными в своих выходах.

И это общее правило, которое применяется к дисперсии:

  • Это безопасно (в смысле LSP) длясделать C[A] ковариант в A IFF A используется только в качестве выхода.
  • Это безопасно (всмысл LSP) сделать C[A] контравариантным в A IFF A используется только в качестве входа.
  • Если A может использоваться как вход или выход, то C[A] должен быть инвариантным в A, в противном случае результат не является безопасным.

Именно поэтому дизайнеры C♯ решили повторно использовать уже существующие ключевые слова in и out для аннотаций отклонений и Kotlin использует те же ключевые слова .

Так, например, неизменяемые коллекции, как правило, могут быть ковариантными по типу элемента, поскольку они не позволяют помещать что-либо в коллекцию (вы можете только построитьct новая коллекция потенциально другого типа), но только для извлечения элементов.Итак, если я хочу получить список чисел, и кто-то передает мне список целых чисел, у меня все в порядке.

С другой стороны, подумайте о выходном потоке (таком как Logger), где вы можете только поместить материал в , но не получить его. Для этого безопасно быть контравариантным. То есть если я ожидаю, что смогу печатать строки, и кто-то протягивает мне принтер, который может печатать любой объект, то он также может печатать строки, и я в порядке. Другими примерами являются функции сравнения (вы помещаете обобщенные значения в , вывод фиксируется как логическое значение, или перечисление, или целое число, или любой другой дизайн, выбранный вашим языком). Или предикаты, они имеют только общие входные данные, выходные данные всегда фиксируются как булевы.

Но, например, коллекции mutable , в которые можно как добавлять, так и извлекать вещи, безопасны только по типу, когда они инвариантны. Существует множество учебных пособий, подробно объясняющих, как нарушить безопасность типов в Java или C♯, например, с помощью их ковариантных изменяемых массивов.

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

Теперь вернемся к вашему вопросу: у вас есть только один стек. Вы никогда не спросите, является ли один стек подтипом другого стека. Таким образом, дисперсия не входит в игру в вашем примере.

0 голосов
/ 12 мая 2018

Одна из неочевидных особенностей дисперсии типов в Scala заключается в том, что аннотации +A и -A фактически говорят нам больше об оболочке, чем о параметре типа.

Допустим,у вас есть поле: class Box[T]

Поскольку T является инвариантом, это означает, что некоторые Box[Apple] не связаны с Box[Fruit].

Теперь давайте сделаем его ковариантным: class Box[+T]

Это делает две вещи, он ограничивает способ, которым код Box может использовать T внутри, но, что более важно, он изменяет отношения между различными экземплярами Boxes.В частности, тип Box[Apple] теперь является подтипом Box[Fruit], поскольку Apple является подтипом Fruit, и мы дали указание Box изменять его отношения типов таким же образом.(то есть "co-") в качестве параметра типа.

... он говорит, что параметр типа должен быть ковариантным +A

На самом деле, что Stackкод не может быть сделан совместно или противопоказанным.Как я уже упоминал, аннотация отклонений добавляет некоторые ограничения на способ использования параметра типа, и что код Stack использует A способами, которые противоречат как ко-, так и противоречивости.

0 голосов
/ 12 мая 2018

Дисперсия больше связана со сложным типом, чем с передачей объектов, что называется подтипом.

Объяснено здесь:

https://en.wikipedia.org/wiki/Covariance_and_contravariance_%28computer_science%29

Если вы хотите создать сложный тип, который принимает некоторый тип как дочерний / родительский элемент списка, который принимает определенный другой тип, тогдаИдея дисперсии приходит в силу.Как и в вашем примере, речь идет о передаче ребенка вместо родителя.Так что это работает.

https://coderwall.com/p/dlqvnq/simple-example-for-scala-covariance-contravariance-and-invariance

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

...