Ответ 1
Я думаю, что у меня есть законное решение, по крайней мере, для некоторой проблемы, указанной здесь. Я имею в виду проблему с List("1").contains(1)
:
https://docs.google.com/document/d/1sC42GKY7WvztXzgWPGDqFukZ0smZFmNnQksD_lJzm20/edit
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 обсуждение темы. Я также добавил комментарии в ответах Ким Стебеля и Питера Шмитца, в которых показано, что их ответы имеют ошибочную функциональность. Таким образом, решения нет. Кроме того, в вышеупомянутой теме обсуждения я объяснил, почему я считаю, что ответ сотовой связи неверен.
Я думаю, что у меня есть законное решение, по крайней мере, для некоторой проблемы, указанной здесь. Я имею в виду проблему с 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.
В моем расширении библиотеки я использую:
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
.
В примерах используется 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
должно по-прежнему проверять соответствие конкретного типа ввода. Эти служебные данные не могут быть удалены компилятором.