Ответ 1
Проблема заключается в том, что Seq
является ковариантным в своем параметре типа. Это имеет большой смысл для большинства его функциональных возможностей. Как непреложный контейнер, он действительно должен быть ковариантным. К сожалению, это мешает, когда они должны определить метод, который принимает некоторый параметризованный тип. Рассмотрим следующий пример:
trait Seq[+A] {
def apply(i: Int): A // perfectly valid
def contains(v: A): Boolean // does not compile!
}
Проблема заключается в том, что функции всегда контравариантны по своим типам параметров и ковариантны по своим возвращаемым типам. Таким образом, метод apply
может возвращать значение типа A
, потому что A
является ковариантным вместе с типом возврата для apply
. Однако contains
не может принимать значение типа A
, потому что его параметр должен быть контравариантным.
Эта проблема может быть решена по-разному. Один из вариантов состоит в том, чтобы просто сделать A
параметр инвариантного типа. Это позволяет использовать его как в ковариантном, так и в контравариантном контексте. Однако этот проект означает, что Seq[String]
не будет подтипом Seq[Any]
. Другой вариант (и наиболее часто используемый) заключается в использовании параметра локального типа, который ограничен ниже ковариантным типом. Например:
trait Seq[+A] {
def +[B >: A](v: B): Seq[B]
}
Этот трюк сохраняет свойство Seq[String] <: Seq[Any]
, а также дает некоторые очень интуитивные результаты при написании кода, который использует гетерогенные контейнеры. Например:
val s: Seq[String] = ...
s + 1 // will be of type Seq[Any]
Результаты функции +
в этом примере представляют собой значение типа Seq[Any]
, поскольку Any
является наименьшей верхней границей (LUB) для типов String
и Int
(другими словами, наименее общий супертип). Если вы думаете об этом, это именно то поведение, которого мы ожидаем. Если вы создаете последовательность с компонентами String
и Int
, то ее тип должен быть Seq[Any]
.
К сожалению, этот трюк, применимый к таким методам, как contains
, дает неожиданные результаты:
trait Seq[+A] {
def contains[B >: A](v: B): Boolean // compiles just fine
}
val s: Seq[String] = ...
s contains 1 // compiles!
Проблема заключается в том, что мы вызываем метод contains
, передавая значение типа Int
. Scala видит это и пытается вывести тип для B
, который является супертипом как Int
, так и A
, который в этом случае создается как String
. LUB для этих двух типов Any
(как показано выше), и поэтому создание экземпляра локального типа для contains
будет Any => Boolean
. Таким образом, метод contains
, по-видимому, не безопасен по типу.
Этот результат не является проблемой для Map
или Set
, потому что ни один из них не является ковариантным в своих типах параметров:
trait Map[K, +V] {
def contains(key: K): Boolean // compiles
}
trait Set[A] {
def contains(v: A): Boolean // also compiles
}
Итак, длинный рассказ, метод contains
на ковариантных типах контейнера не может быть ограничен, чтобы принимать значения типа компонента только из-за того, как работают типы функций (контравариантные по своим типам параметров). На самом деле это не ограничение Scala или плохой реализации, это математический факт.
Утешающий приз состоит в том, что на практике это действительно не проблема. И, как уже упоминалось в других ответах, вы всегда можете определить свое собственное неявное преобразование, которое добавляет метод типа "type-safe" contains
, если вам действительно нужна дополнительная проверка.