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)