Scala Обозначения ковариации и нижнего типа Объяснение
Я пытаюсь склонить голову ковариации в отношении методов, создающих новые неизменяемые типы, используя нижние границы
class ImmutableArray[+T](item: T, existing: List[T] = Nil) {
private val items = item :: existing
def append[S >: T](value: S) = new ImmutableArray[S](value, items)
}
Я понимаю, что параметр типа T
не может использоваться в методе добавления, поскольку он нарушает правила, но если я скажу Customer
и подкласс Student
, я все же могу сделать тип U
Student
.
Я вижу, что это работает, но почему это не является нарушением правил? Я мог понять, был ли у меня список Student
, а затем добавил Customer
, я мог только вернуть список Customer
из-за того, что не был назначен Customer
для Student
, поскольку он является родительским тип. Но почему я могу использовать Student
?
Что мне не хватает?
Спасибо Блэр
Ответы
Ответ 1
Ваш класс предлагает 2 операции с участием T:
-
Строительство
nextImmutableArray = new ImmutableArray(nextT, priorImmutableArray)
Из-за этой операции параметр типа T должен быть ковариантным: + T. Это позволяет построить с параметром, установленным для объекта типа (T ИЛИ подтип T).
Подумайте: он действителен, чтобы построить массив апельсинов, включив оранжевый Валенсией.
-
Комбинация
nextImmutableArray.append(newItemTorAncestor)
Этот метод не добавляется к вашей структуре данных. Он принимает два независимых элемента (ваш массив этот и дополнительный объект), и он объединяет их в новом массиве. Вы можете изменить имя метода на appendIntoCopy. Еще лучше, вы можете использовать имя +. Но чтобы быть наиболее правильным и согласным с Scala соглашениями, лучшим именем было бы : +.
Почему я waffling о "случайном" имени метода, когда вы задали конкретный вопрос???
Поскольку точный характер метода определяет, является ли возвращенная структура данных (а) невариантной с T (b), ко-вариантной с T (c), контравариантной с T.
- Начать с: ImmutableArray [T] - содержит тип T (или подтипы)
- Объединить с: Object типа S.
- Результат: ImmutableArray [S]
- Если S было разрешено быть надлежащим подтипом T (за пределами самого T), то новый массив не может содержать исходные элементы типа T!
- Если S имеет тип T или супертип T, то все хорошо - может содержать исходные элементы, а также новый элемент!
При объединении массивов и элементов вновь созданная структура данных должна иметь параметр типа, который является супертипом общего типа предка. В противном случае он не может содержать исходные элементы. В общем случае, когда вы выполняете "a: + b", где A является массивом [A], а b имеет тип B, результирующая структура данных - это Array [Some_SuperType_Of_Both_A_and_B].
Подумайте: если я начну с массива апельсинов, то добавьте лимон, я получаю массив цитрусовых фруктов (не апельсинов, апельсинов пупка и лимонов).
Правила метода (строгие на входе, размещаемые на выходе):
- a) входной параметр предоставляет элемент для вставки (мутации): Со-Вариант
- a) выходной параметр возвращает элемент из структуры данных: Contra-Variant
- c) выходной параметр, возвращает структуру данных после объединения: Contra-Variant
- c) Использовать тип как нижнюю границу: Отклонить дисперсию ( "Contra-variant to T" = "Co-Variant to S, у которого есть нижняя граница T" )
В случае добавления: Начать с T, Структура выходных данных = Contra-Variant to T, Тип S использует T как нижнюю границу, поэтому Input Parameter = Co-Variant с S. Это означает, что если T1 является подтипом T2, то ImmutableArray [T1] является подтипом ImmutableArray [T2] и что он может быть заменен везде, где ожидается последний, со всеми методами, вытекающими из принципа замены Лискова.
Ответ 2
Первый вопрос:
Я понимаю, что параметр типа T нельзя использовать в методе добавления, поскольку он нарушает правила
Ну, он может быть использован. S >: T
просто означает, что если вы передадите тип S
, равный T
или его парант, тогда будет использоваться S
. Если вы передадите тип, который является подуровнем до T
, то будет использоваться T
.
scala> class Animal
defined class Animal
scala> class Canine extends Animal
defined class Canine
scala> class Dog extends Canine
defined class Dog
scala> new ImmutableArray[Canine](new Canine)
res6: ImmutableArray[Canine] = [email protected]
scala> res6.append(new Animal)
res7: ImmutableArray[Animal] = [email protected]
scala> res6.append(new Canine)
res8: ImmutableArray[Canine] = [email protected]
scala> res6.append(new Dog)
res9: ImmutableArray[Canine] = [email protected]
Выше res6.append(new Dog)
все еще дает вам ImmutableArray типа Canine. И если вы думаете в некотором смысле, это имеет смысл, так как добавление Dog в Canine Array по-прежнему будет содержать массив Canine. Но добавление Animal to Canine Array делает его Animal, поскольку он уже не может быть идеально собачьим (может быть молярным или что-то еще).
Это прекрасный пример того, почему обычно известно, что объявление контра-вариантного типа делает его идеальным для записи (ваш случай) и ковариации для чтения.
В вашем примере, я думаю, что путаница может быть связана с тем, что вы сравниваете S >: T
с S super T
(из java-мира). С S super T
вы должны иметь тип аргумента, который является классом Super T
, и он не позволяет передавать аргумент, который является подтипом, на T
. В scala компилятор позаботится об этом (благодаря типу).
Ответ 3
Рассмотрим иерархию followng:
class Foo
class Bar extends Foo { def bar = () }
class Baz extends Bar { def baz = () }
И класс, похожий на ваш:
class Cov[+T](val item: T, val existing: List[T] = Nil) {
def append[S >: T](value: S) = new Cov[S](value, item :: existing)
}
Затем мы можем построить три экземпляра для каждого из подтипов Foo
:
val cFoo = new Cov(new Foo)
val cBar = new Cov(new Bar)
val cBaz = new Cov(new Baz)
И тестовая функция, требующая элементов bar
:
def test(c: Cov[Bar]) = c.item.bar
Он содержит:
test(cFoo) // not possible (otherwise `bar` would produce a problem)
test(cBaz) // ok, since T covariant, Baz <: Bar --> Cov[Baz] <: Cov[Bar]; Baz has bar
Теперь метод append
, возвращаясь к верхней границе:
val cFoo2 = cBar.append(new Foo)
Это нормально, потому что Foo >: Bar
, List[Foo] >: List[Bar]
, Cov[Foo] >: Cov[Bar]
.
Теперь правильно ваш bar
доступ пропал:
cFoo2.item.bar // bar is not a member of Foo
Чтобы понять, зачем вам нужна верхняя граница, представьте себе следующее:
class Cov[+T](val item: T, val existing: List[T] = Nil) {
def append(value: T) = new Cov[T](value, item :: existing)
}
class BarCov extends Cov[Bar](new Bar) {
override def append(value: Bar) = {
value.bar // !
super.append(value)
}
}
Тогда вы могли бы написать
def test2[T](cov: Cov[T], elem: T): Cov[T] = cov.append(elem)
И допустим следующее недопустимое поведение:
test2[Foo](new BarCov, new Foo) // BarCov <: Cov[Foo]
где value.bar
будет вызываться на Foo
. Используя (правильно) верхнюю границу, вы не сможете реализовать append
как в гипотетическом последнем примере:
class BarCov extends Cov[Bar](new Bar) {
override def append[S >: Bar](value: S) = {
value.bar // error: value bar is not a member of type parameter S
super.append(value)
}
}
Таким образом, система типов остается звуковой.
Ответ 4
Это работает, потому что метод append возвращает более широкий класс, чем оригинальный.
Позвольте провести небольшой эксперимент.
scala> case class myIntClass(a:Int)
defined class myIntClass
scala> case class myIntPlusClass(a:Int, b:Int)
defined class myIntPlusClass
scala> class ImmutableArray[+T](item: T, existing: List[T] = Nil){
|
| private val items = item :: existing
|
| def append[S >: T](value: S) = new ImmutableArray[S](value,items)
| def getItems = items
| }
defined class ImmutableArray
scala> val ia = new ImmutableArray[myIntClass](myIntClass(3))
ia: ImmutableArray[myIntClass] = [email protected]
scala> ia.getItems
res15: List[myIntClass] = List(myIntClass(3))
scala> ia.append(myIntPlusClass(3,5))
res16: ImmutableArray[Product with Serializable] = [email protected]
scala> res16.getItems
res17: List[Product with Serializable] = List(myIntPlusClass(3,5), myIntClass(3))
scala> res16
res18: ImmutableArray[Product with Serializable] = [email protected]
Итак, здесь вы можете добавить производный класс, но он работает только из-за того, что базовый тип результирующего массива понижается до самого низкого общего знаменателя (в этом случае Serializable).
Если мы попытаемся заставить производный тип на результирующем массиве, он не будет работать:
scala> ia.append[myIntPlusClass](myIntPlusClass(3,5))
<console>:23: error: type arguments [myIntPlusClass] do not conform to method append type parameter bounds [S >: myIntClass]
ia.append[myIntPlusClass](myIntPlusClass(3,5))
Попытка сделать то же самое, что делает append, возвращает массив производных типов, не будет работать, потому что T не является подклассом S:
scala> class ImmutableArray[+T](item: T, existing: List[T] = Nil){
|
| private val items = item :: existing
|
| def append[S <: T](value: S) = new ImmutableArray[S](value,items)
| def getItems = items
| }
<console>:21: error: type mismatch;
found : List[T]
required: List[S]
def append[S <: T](value: S) = new ImmutableArray[S](value,items)