Что делает [B>: A] в Scala?

Что означает [B >: A] в Scala? И каковы эффекты?

Пример ссылки: http://www.scala-lang.org/node/129

class Stack[+A] {
    def push[B >: A](elem: B): Stack[B] = new Stack[B] {
        override def top: B = elem
        override def pop: Stack[B] = Stack.this
        override def toString() = elem.toString() + " " + Stack.this.toString()
    }
    def top: A = error("no element on stack")
    def pop: Stack[A] = error("no element on stack")
    override def toString() = ""
}

object VariancesTest extends Application {
    var s: Stack[Any] = new Stack().push("hello");
    s = s.push(new Object())
    s = s.push(7)
    println(s)
}

Ответы

Ответ 1

[B >: A] является нижней границей. Это означает, что B ограничивается супертипом A.

Аналогично [B <: A] является границей верхнего типа, что означает, что B является подтипом A.

В примере, который вы показали, вы можете нажать элемент типа B на стек, содержащий элементы A, но результатом является стек элементов B.

Страница, на которой вы видели это, имеет ссылку на другую страницу нижние границы типов, которая включает пример, показывающий эффект.

Ответ 2

X <: Y означает, что параметр типа X должен быть подтипом типа Y. X >: Y означает противоположность, X должен быть супер-типом Y (в обоих случаях X = Y нормально). Эта нотация может быть противоположной интуиции, можно подумать, что собака больше, чем животное (точнее в терминах программирования, больше услуг), но по той причине, что это более точно, есть меньше собак, чем есть животные, тип Animal содержит больше значений, чем тип Dog, он содержит всех собак и всех страусов. Итак Animal > : Dog.

По той причине, что push имеет эту подпись, я не уверен, что смогу объяснить ее лучше, чем страница, откуда приходит пример, но позвольте мне попробовать.

Он начинается с отклонения. + в class Stack[+A] означает, что Stack есть covariant in A. если X является подтипом Y, Stack[X] будет подтипом Stack[Y]. Стопка собак - также стопка животных. Для математически наклонного, если вы видите Stack как функцию от типа к типу (X - это тип, если вы передаете его в Stack, вы получаете Stack [X], что является другим типом), будучи ковариантным, означает, что он увеличивается function (with <:, отношение подтипирования - это порядки по типам).

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

def push(a: A): Unit

(пример отличается, push возвращает новый стек, оставляя this неизменным). Конечно, Stack [Dog] должен только принимать собак, чтобы их вставляли. В противном случае это уже не будет стопкой собак. Но если мы признаем, что его рассматривают как стопку животных, мы могли бы сделать

val dogs : Stack[Dog] = new Stack[Dog]
val animals : Stack[Animal] = dogs // if we say stack is covariant
animals.push(ostrich) // allowed, we can push anything in a stack of any. 
val topDog: Dog = dogs.top  // ostrich!

Ясно, что обработка этого стека как ковариантная является необоснованной. Когда стек рассматривается как Stack[Animal], допускается операция, которая не была бы на Stack[Dog]. То, что было сделано здесь с помощью push, может быть выполнено с помощью любой процедуры, которая воспринимает A как аргумент. Если общий класс отмечен как ковариантный, с C [+ A], то A не может быть типом любого аргумента любой (общедоступной) подпрограммы C, и компилятор будет применять это.

Но стек в примере отличается. Мы имели бы def push(a: A): Stack[A]. Если вы вызываете push, вы получаете новый стек, а исходный стек остается неизменным, он по-прежнему является правильным Stack [Dog], что бы ни было нажато. Если мы делаем

val newStack = dogs.push(ostrich)

dogs по-прежнему остается неизменным и остается Stack[Dog]. Очевидно, что newStack нет. Это также не Stack[Ostrich], поскольку он также содержит собак, которые были (и остаются) в исходном стеке. Но это было бы правильным Stack[Animal]. Если кто-то подталкивает кошку, было бы точнее сказать, что это Stack[Mammal] (будучи также стаей животных). Если кто-то нажимает 12, это будет только Stack[Any], единственный общий супертип Dog и Integer. Проблема заключается в том, что компилятор не знает, что этот вызов безопасен и не допустит аргумент a: A в def push(a: A): Stack[A], если Stack отмечен ковариантным. Если бы он остановился там, ковариантный стек был бы бесполезен, потому что в нем не было бы возможности вносить в него значения.

Подпись решает проблему:

def push[B >: A](elem: B): Stack[B]

Если B является предком A, при добавлении B получается Stack[B]. Поэтому добавление Mammal в Stack[Dog] дает Stack[Mammal], добавление животного дает Stack[Animal], что прекрасно. Добавление собаки тоже нормально, A > : A истинно.

Это хорошо, но кажется слишком ограничительным. Что делать, если добавленный тип элемента не является предком A? Например, что, если это потомок, например, dogs.push(goldenRetriever). Нельзя взять B = GoldenRetriever, не имеет значения GoldenRetriever >: Dog, а наоборот. Тем не менее, можно взять B = Собака в порядке. Предполагается, что параметр elem будет иметь тип Dog, мы можем пройти, конечно, мимо GoldenRetriever. Один получает стопку B, все еще стопку собак. И правильно, что B = GoldenRetriever не разрешено. Результат был бы напечатан как Stack[GoldenRetriever], что было бы неправильно, потому что в стеке также могли содержаться ирландские сеттеры.

Как насчет острий? Ну Ostrich не является ни супертипом, ни подтипом Dog. Но так же, как можно добавить золотого ретривера, потому что это собака, и можно добавить собаку, страус - это животное, и можно добавить животное. Поэтому, принимая B = Animal > : Dog работает, и поэтому a при нажатии страуса получает Stack[Animal].

Создание ковариационной силы стека этой сигнатурой, более сложной, чем наивная push(a: A) : Stack[A]. Но мы получаем рутину, которая является полностью гибкой, все может быть добавлено, а не только A, и тем не менее, выводит результат как можно точнее. И фактическая реализация, за исключением объявлений типов, такая же, как и при использовании push(a: A).