Выборочно отключить подпотребление в Scala? (правильно введите List.contains)
List("a").contains(5)
Потому что Int
никогда не может содержаться в списке String
, это должно генерировать ошибку во время компиляции, но это не так.
Тщательно и бесшумно проверяет каждый String
содержится в списке на равенство 5
, который никогда не может быть правдой ("5"
никогда не равняется 5
в Скале).
Это было названо "проблема" содержит ". И некоторые подразумевают, что если система типов не может правильно вводить такую семантику, то зачем предпринимать дополнительные усилия для принудительного применения типов. Поэтому я считаю, что это важная проблема, которую нужно решить.
Тип параметризации 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
только те типы, которые имеют метод равных, который 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 вызывает оператор::.
Итак, мой вопрос, что является лучшим решением этой проблемы?
Мой предварительный вывод заключается в том, что неявное подчинение должно быть отключено на сайте определения, где семантика иначе не набрана правильно. Я предоставлю ответ, который показывает, как отключить неявное подчинение на сайте определения метода. Есть ли альтернативные решения?
Обратите внимание, что эта проблема носит общий характер и не изолирована от списков.
ОБНОВЛЕНИЕ: Я подал запрос на улучшение и начал обсуждение в Scala. Я также добавил комментарии к ответам Ким Стебель и Питера Шмитца, показывающие, что их ответы имеют ошибочную функциональность. Таким образом, нет решения. Также в вышеупомянутой ветке обсуждения я объяснил, почему я думаю, что ответ сока неверен.
6 ответов
Я думаю, что у меня есть законное решение, по крайней мере, некоторые из проблем, размещенных здесь - я имею в виду, проблема с List("1").contains(1)
: https://docs.google.com/document/d/1sC42GKY7WvztXzgWPGDqFukZ0smZFmNnQksD_lJzm20/edit
Это звучит хорошо в теории, но, на мой взгляд, разваливается в реальной жизни.
equals
не основывается на типах и contains
строит поверх этого.
Вот почему код, как 1 == BigInt(1)
работает и возвращает результат, который ожидают большинство людей.
На мой взгляд, не имеет смысла делать contains
более строгий, чем equals
,
Если contains
будет сделано более строгим, код как List[BigInt](1,2,3) contains 1
перестал бы работать полностью.
Кстати, я не думаю, что "небезопасно" или "небезопасно" являются правильными терминами.
Почему бы не использовать класс типов равенства?
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===)
Я думаю, что вы неправильно поняли решение Мартина, это не 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.
Примеры использования 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
.]
ОБНОВЛЕНИЕ 03 мая 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)
^
Единственный способ получить желаемую форму подчинения, это также приведение к методу (вызову) use-site.
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
Согласно ответу soc, текущая семантика для List.contains
является то, что ввод должен быть равен, но не обязательно супертип содержимого элемента. Это предполагает List.contains
обещает, что любой совпадающий элемент равен только и не обязан быть (подтипом или) копией экземпляра ввода. Текущий универсальный интерфейс равенства Any.equals : Any => Boolean
является однотипной, поэтому равенство не навязывает отношения подтипа. Если это желаемая семантика для List.contains
отношения подтипов не могут быть использованы для оптимизации семантики во время компиляции, например, отключение неявного подчинения, и мы застряли с потенциальной семантической неэффективностью, которая ухудшает производительность во время выполнения для List.contains
,
В то время как я буду больше изучать и размышлять о равенстве и содержании, мой ответ остается верным для общей цели выборочного отключения неявного подчинения на сайте определения метода.
Мой мыслительный процесс также продолжается целостно в отношении лучшей модели равенства.
Обновление: я добавил комментарий ниже ответа соц, так что теперь я думаю, что его точка зрения не имеет значения. Равенство всегда должно основываться на подтипных отношениях, которые, как ни странно, предлагает Мартин Одерский для нового пересмотра равенства (см. Также его версию contains
). Любая специальная полиморфная эквивалентность (например, BitInt(1) == 1
) может быть обработано с неявными преобразованиями. Я объяснил в своем комментарии ниже ответ Didierd, что без моего улучшения ниже, afaics предложили Мартина 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
на интерфейсе (иначе trait
).
Таким образом Any.equals
override должен по-прежнему проверять, соответствует ли конкретный тип ввода. Эти накладные расходы не могут быть удалены компилятором.
В моем расширении библиотеки я использую:
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
,