Выборочно отключить подсемейство в Scala?(правильно введите List.contains) - PullRequest
22 голосов
/ 02 декабря 2011
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 вызывает :: оператор .

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

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

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

ОБНОВЛЕНИЕ : Я подал запрос на улучшение и начал обсуждение темы scala на этом . Я также добавил комментарии к ответам Ким Стебель и Питера Шмитца, показывающие, что их ответы имеют ошибочную функциональность. Таким образом, нет решения. Также в вышеупомянутой ветке обсуждения я объяснил, почему я думаю, что ответ сока неверен.

Ответы [ 6 ]

12 голосов
/ 02 декабря 2011

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

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

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

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

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

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

8 голосов
/ 02 декабря 2011

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

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 голосов
/ 03 декабря 2011

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

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

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

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

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

3 голосов
/ 03 декабря 2011

В примерах используется 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 , что без моего улучшения ниже, предложенный Мартином 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)
}

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

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

3 голосов
/ 02 декабря 2011

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

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.

2 голосов
/ 18 сентября 2013

Я думаю, что у меня есть законное решение, по крайней мере, для части проблемы, опубликованной здесь - я имею в виду проблему с List("1").contains(1): https://docs.google.com/document/d/1sC42GKY7WvztXzgWPGDqFukZ0smZFmNnQksD_lJzm20/edit

...