Почему Seq.contains принимает тип Any, а не параметр типа A?

Например:

scala> val l:List[String] = List("one", "two")
l: List[String] = List(one, two)

scala> l.contains(1) //wish this didn't compile
res11: Boolean = false 

Различные объяснения того, почему все было сделано таким образом в Java, похоже, не так сильно применимы, как Map и Set делают реализацию типа безопасной версии из contains и друзей. Есть ли способ сделать безопасный тип contains на Seq, чтобы клонировать его в Set?

Ответы

Ответ 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, если вам действительно нужна дополнительная проверка.

Ответ 2

Я не уверен, почему все было спроектировано таким образом - возможно, чтобы что-то отразить в Java.

В любом случае, более эффективно использовать шаблон pimp-my-library, чем клонировать в набор:

class SeqWithHas[T](s: Seq[T]) {
  def has(t: T) = s.contains(t)
}
implicit def seq2seqwithhas[T](s: Seq[T]) = new SeqWithHas(s)

scala> List("3","5") has 1
<console>:7: error: type mismatch;
 found   : Int(1)
 required: java.lang.String
       List("3","5") has 1
                         ^

scala> List("3","5") has "1"
res1: Boolean = false

(Вероятно, вы захотите поместить этот материал и другие полезные вещи в один объект, а затем импортировать MyHandyObject._ в большинстве ваших исходных файлов.)

Ответ 3

Если вы хотите отказаться от инфикса в пользу регулярного вызова метода, определение и импорт следующего метода (...) позволит избежать создания экземпляра каждый раз, когда вам понадобится тест типа "есть" (стоит во внутренних петлях, например):

def has[T](s: Set[T], t: T) = s.contains(t)

Естественно, Set [T] может быть ослаблен до наименее специфического типа, который имеет метод contains.

Ответ 4

Предоставляя доказательства равенства типов,

def contains[A,B](xs: List[A], x: B)(implicit ev: A =:= B) = xs.contains(x)