Выборочно отключить включение в Scala? (правильно введите List.contains)

List("a").contains(5)

Поскольку Int никогда не может содержаться в списке String, этот должен генерировать ошибку во время компиляции, но это не так.

Он расточительно и молча проверяет каждый String, содержащийся в списке, для равенства 5, который никогда не может быть правдой ("5" никогда не равен 5 в Scala).

Это было названо " проблема" содержит "". И некоторые подразумевали, что если система типов не может правильно напечатать такую ​​семантику, то зачем вам прибегать к дополнительным усилиям по обеспечению соблюдения типов. Поэтому я считаю, что это важная проблема.

Параметризация типа B >: A List.contains вводит любой тип, который является супертипом типа A (тип элементов, содержащихся в списке).

trait List[+A] {
   def contains[B >: A](x: B): Boolean
}

Эта параметризация необходима, потому что +A объявляет, что список ковариант для типа A, поэтому A не может использоваться в контравариантная позиция, т.е. тип входного параметра. Ковариантные списки (которые должны быть неизменными) гораздо более мощные для расширения, чем инвариантные списки (которые могут быть изменчивыми).

A является String в проблемном примере выше, но Int не является супертипом String, так что случилось? неявное предположение в Scala, решил, что Any является взаимным супертипом как String, так и Int.

Создатель Scala, Мартин Одерски, предложил, что исправление будет заключаться в том, чтобы ограничить тип ввода B только теми типы, которые имеют метод equals, который Any не имеет.

trait List[+A] {
   def contains[B >: A : Eq](x: B): Boolean
}

Но это не решает проблему, потому что два типа (где тип ввода не является супертипом типа элементов списка) могут иметь взаимный супертип, который является подтипом Any, то есть также подтип Eq. Таким образом, он будет компилироваться без ошибок, и неверно типизированная семантика останется.

Отключение неявного подзапроса каждый раз, когда не является идеальным решением, так как неявное предположение является причиной того, что следующий пример для включения в Any работает, И мы не хотим, чтобы вас принуждали использовать приведения типов, когда принимающий сайт (например, передавая как аргумент функции) правильно набрал семантику для взаимного супертипа (это может быть даже не Any).

trait List[+A] {
   def ::[B >: A](x: B): List[B]
}

val x : List[Any] = List("a", 5) // see[1]

[1] List.apply вызывает оператор::.

Итак, мой вопрос в том, что является лучшим решением этой проблемы?

Мой предварительный вывод состоит в том, что неявное предположение должно быть отключено на сайте определения, где семантика в противном случае неверно набрана. Я дам ответ, который показывает, как отключить неявное подчинение на сайте определения метода. Существуют ли альтернативные решения?

Обратите внимание, что эта проблема является общей и не изолирована от списков.

UPDATE: отправил запрос на улучшение и начал scala обсуждение темы. Я также добавил комментарии в ответах Ким Стебеля и Питера Шмитца, в которых показано, что их ответы имеют ошибочную функциональность. Таким образом, решения нет. Кроме того, в вышеупомянутой теме обсуждения я объяснил, почему я считаю, что ответ сотовой связи неверен.

Ответы

Ответ 2

Это звучит хорошо в теории, но, по моему мнению, в реальной жизни разваливается.

equals не основан на типах и contains строит поверх этого.

Вот почему код типа 1 == BigInt(1) работает и возвращает результат, ожидаемый большинством людей.

По-моему, не имеет смысла делать contains более строгим, чем equals.

Если contains будет более строгим, код, например List[BigInt](1,2,3) contains 1, перестанет работать полностью.

Я не думаю, что "опасные" или "не безопасные типы" - это правильные условия, кстати.

Ответ 3

Почему бы не использовать класс классов соответствия?

scala> val l = List(1,2,3)
l: List[Int] = List(1, 2, 3)

scala> class EQ[A](a1:A) { def ===(a2:A) = a1 == a2 } 
defined class EQ

scala> implicit def toEQ[A](a1:A) = new EQ(a1)
toEQ: [A](a1: A)EQ[A]

scala> l exists (1===)
res7: Boolean = true

scala> l exists ("1"===)
<console>:14: error: type mismatch;
 found   : java.lang.String => Boolean
 required: Int => Boolean
              l exists ("1"===)
                           ^

scala> List("1","2")
res9: List[java.lang.String] = List(1, 2)

scala> res9 exists (1===)
<console>:14: error: type mismatch;
 found   : Int => Boolean
 required: java.lang.String => Boolean
              res9 exists (1===)

Ответ 4

Я думаю, вы неправильно поняли решение Мартина, это не B <: Eq, это B : Eq, что является ярлыком для

def Contains[B >: A](x: B)(implicit ev: Eq[B])

И Eq[X] затем будет содержать метод

def areEqual(a: X, b: X): Boolean

Это не то же самое, что перемещение метода equals Any, немного меньшего в иерархии, что действительно не решило бы ни одной проблемы его наличия в Any.

Ответ 5

В моем расширении библиотеки я использую:

class TypesafeEquals[A](val a: A) {
  def =*=(x: A): Boolean = a == x
  def =!=(x: A): Boolean = a != x
}
implicit def any2TypesafeEquals[A](a: A) = new TypesafeEquals(a)


class RichSeq[A](val seq: Seq[A]) { 
  ...
  def containsSafely(a: A): Boolean = seq exists (a =*=)
  ...
}
implicit def seq2RichSeq[A](s: Seq[A]) = new RichSeq(s)

Поэтому я не звоню contains.

Ответ 6

В примерах используется L вместо List или SeqLike, потому что для того, чтобы это решение применялось к существующим методам contains для этих коллекций, для этого потребовалось бы изменение существующего кода библиотеки. Одна из целей - лучший способ сделать равенство, а не лучший компромисс для взаимодействия с текущими библиотеками (хотя необходимо учитывать обратную совместимость). Кроме того, моя другая цель заключается в том, что этот ответ обычно применим для любой функции метода, которая по какой-либо причине хочет выборочно отключить функцию неявного подчинения компилятора Scala, не обязательно привязанного к семантике равенства.

case class L[+A]( elem: A )
{
   def contains[B](x: B)(implicit ev: A <:< B) = elem == x
}

Приведенная выше генерирует ошибку по желанию, предполагая, что желаемая семантика для List.contains - вход должен быть равен и супертип содержащегося элемента.

L("a").contains(5)
error: could not find implicit value for parameter ev: <:<[java.lang.String,Int]
       L("a").contains(5)
                      ^

Ошибка не возникает, когда неявное предположение не требуется.

scala> L("a").contains(5 : Any)
defined class L

scala> L("a").contains("")
defined class L

Это отключает неявное подчинение (выборочно на сайте определения метода), требуя, чтобы тип входного параметра B был таким же, как тип аргумента, переданный как вход (т.е. неявно подлежит включению с A), а затем отдельно требуют неявных доказательств, что B является a или имеет неявно неопределяемый супертип A.]


ОБНОВЛЕНИЕ 3 мая 2012 г.: приведенный выше код не является полным, как показано ниже, что отключение всех предположений на сайте определения метода не дает желаемого результата.

class Super
defined class Super
class Sub extends Super
defined class Sub
L(new Sub).contains(new Super)
defined class L
L(new Super).contains(new Sub)
error: could not find implicit value for parameter ev: <:<[Super,Sub]
       L(new Super).contains(new Sub)
                            ^

Единственный способ получить желаемую форму предположения - это также использовать метод использования (вызова).

L(new Sub).contains(new Super : Sub)
error: type mismatch;
 found   : Super
 required: Sub
       L(new Sub).contains(new Super : Sub)
                           ^
L(new Super).contains(new Sub : Super)
defined class L

Per soc answer, текущая семантика для List.contains заключается в том, что вход должен быть равен, но не обязательно супертипу содержащегося элемента. Предполагается, что List.contains promises любой согласованный элемент равен только и не обязательно должен быть (подтипом или) копией экземпляра ввода. Нынешний универсальный интерфейс равенства Any.equals : Any => Boolean является единичным, поэтому равенство не обеспечивает отношения подтипирования. Если это желаемая семантика для List.contains, отношения подтипирования не могут использоваться для оптимизации семантики времени компиляции, например. отключая неявное подразделение, и мы застряли в потенциальной семантической неэффективности, которая ухудшает производительность во время выполнения List.contains.

Пока я буду изучать и больше думать о равенстве и содержать, afaics мой ответ остается в силе для общей цели выборочного отключения неявного подчинения на сайте определения метода.

Мой мыслительный процесс также продолжается holistically w.r.t. лучшая модель равенства.


Обновление. Я добавил комментарий ниже soc answer, поэтому теперь я думаю, что его точка не актуальна. Равенство всегда должно основываться на подтипированной связи, а именно, что Мартин Одерски предлагает для нового процесса капитального ремонта (см. Также его версия contains). Любая ad-hoc полиморфная эквивалентность (например, BitInt(1) == 1) может обрабатываться с неявными преобразованиями. Я объяснил в своем комментарии ниже ответ didierd, что без моего улучшения ниже, afaics Martin предположил, что contains будет иметь семантическую ошибку, при которой взаимно неявно добавленный супертип (кроме Any) выберет неправильный неявный экземпляр Eq (если он существует, иначе ненужная ошибка компилятора). Мое решение отключает неявное предположение для этого метода, что является правильной семантикой для подтипированного аргумента Eq.eq.

trait Eq[A]
{
   def eq(x: A, y: A) = x == y
}

implicit object EqInt extends Eq[Int]
implicit object EqString extends Eq[String]

case class L[+A]( elem: A )
{
   def contains[B](x: B)(implicit ev: A <:< B, eq: Eq[B]) = eq.eq(x, elem)
}
L("a").contains("")

Примечание Eq.eq может быть необязательно заменено на implicit object (не переопределено, потому что нет виртуального наследования, см. ниже).

Обратите внимание, что по желанию L("a").contains(5 : Any) больше не компилируется, поскольку Any.equals больше не используется.

Мы можем сокращать.

case class L[+A]( elem: A )
{
   def contains[B : Eq](x: B)(implicit ev: A <:< B) = eq.eq(x, elem)
}

Добавить. x == y должен быть виртуальным вызовом наследования, т.е. x.== должен быть объявлен override, потому что существует нет виртуального наследования в классе Eq. Параметр типа A является инвариантным (поскольку A используется в контравариантной позиции в качестве входного параметра Eq.eg). Затем мы можем определить implicit object на интерфейсе (a.k.a. trait).

Таким образом, переопределение Any.equals должно по-прежнему проверять соответствие конкретного типа ввода. Эти служебные данные не могут быть удалены компилятором.