Выборочно отключить подчинение в 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 вызывает оператор ::.

Итак, мой вопрос: как лучше всего решить эту проблему?

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

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

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


person Shelby Moore III    schedule 02.12.2011    source источник
comment
Поскольку мой представитель <100, мне нужно подождать 8 часов, прежде чем я смогу опубликовать свой ответ на свой вопрос.   -  person Shelby Moore III    schedule 02.12.2011


Ответы (6)


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

person Vlad Patryshev    schedule 18.09.2013
comment
К сожалению, ваш ответ несколько неверен и обеспечивает только противоположную функциональность, как у Ким Стебель и моего ответа. (new Super) contains (new Sub) правильно не вызывает ошибки. Однако (new Sub) contains (new Super) не компилируется! Та же, но противоположная проблема, которую я обнаружил в своем ответе, ср. типы Super и Sub в моем ответе. И ваше решение имеет высокие накладные расходы, а решение Stebel не так хорошо интегрировано, поэтому мой ответ пока остается лучшим. Единственное решение - это то, как я описал свое ОБНОВЛЕНИЕ по моему вопросу. Я проголосовал за вашу смекалку. - person Shelby Moore III; 19.09.2013
comment
Я решил, что ваш ответ лучший (из худшего), потому что, хотя он имеет большие накладные расходы и не позволяет вводить супертипы, он единственный, который позволяет нам разумно провести своего рода эксперимент с выборочным отключением включения. - person Shelby Moore III; 19.09.2013
comment
Решение без резюме, которое полностью зависит от другого сайта, не близко к оптимальному. - person user unknown; 22.09.2013

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

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

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

На мой взгляд, нет смысла делать contains строже, чем equals.

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

Я, кстати, не думаю, что «небезопасный» или «небезопасный по типу» здесь правильные термины.

person soc    schedule 02.12.2011
comment
Это зависит от семантики List.contains. Если это задокументировано, чтобы проверить, действительно ли список содержит ввод, например BitInt, то отключение неявного подчинения - правильное решение. Если это задокументировано, чтобы проверить, соответствует ли какой-либо элемент списка входным данным, то ваша точка зрения действительна. Я бы предпочел другой метод containsEqual. Равенство планируется улучшить, возможно, Сопоставимое решение лучше. - person Shelby Moore III; 02.12.2011
comment
Я отредактировал свой вопрос и изменил тип unsafe на правильно набранную семантику в соответствии с вашим предложением. Спасибо. - person Shelby Moore III; 02.12.2011
comment
Честно говоря, List[BigInt](1,2,3) не содержит 1, он содержит BigInt(1). Так же, как List('1','2','3') не содержит 1. Поэтому, когда вы говорите, что в реальной жизни разваливается, вы в основном говорите, что безопасность типов не всегда удобна. Что, безусловно, правда. Но это вопрос безопасности типов, имхо. - person Dan Burton; 04.12.2011
comment
Я знаю это, но какова семантическая разница между 1 и BigInt(1)? Имхо, нет. Оба представляют идею 1. Базовая реализация здесь не играет роли. - person soc; 04.12.2011
comment
Отказ от типизации @soc обеспечивает единую семантику (например, эта проблема с Any.equals). Int не может держать BigInt. Int может быть подтипом BigInt. Похоже, это не семантика, например один и тот же пользовательский оператор или имя метода, определенное в двух классах, не обязательно имеет одинаковую семантику на сайте использования (потому что он не имеет того же типа для this). - person Shelby Moore III; 04.12.2011
comment
Если BigInt(1) == 1, то я думаю, что он также должен придерживаться этого BigInt(1) + someLargeValue == 1 + someLargeValue. Что в целом неверно. 1 и BigInt(1) разные. (Хотя лично я хотел бы, чтобы буквальный 1 разрешался в неограниченную BigInt реализацию и, возможно, использовал 1L и 1I как литералы типа Int64 и Int32.) - person Debilski; 04.12.2011
comment
@debilski Это свойство должно сохраняться не потому, что BigInt(1) == 1. отношение эквивалентности для == не требует добавления этого свойства. Это свойство будет истинным при условии, что Int.+ : BigInt => BigInt существует, и я отметил, что + может выглядеть одинаково на сайте использования, но имеет другой тип для this (и, следовательно, другую денотационную семантику). - person Shelby Moore III; 04.12.2011
comment
@soc List[BigInt](1,2,3).contains(4) медленнее, чем List[BigInt](1,2,3).contains(BigInt(4)) в текущей реализации contains, потому что BigInt.equals (x: BigInt) равен быстрее, чем BigInt.equals (x: Any). Поскольку существует неявное преобразование Int => BigInt, мой ответ не разваливается и работает быстрее, чем текущая реализация contains, потому что он вызывает неявное преобразование для List[BigInt](1,2,3).contains(4). - person Shelby Moore III; 04.12.2011
comment
Вы в основном утверждаете, что отказ от набора текста (отнесение к Any) не имеет значения. Также этот чрезмерно усердный (или, по крайней мере, не отключаемый выборочно) Scala подчинение вызывает более общую проблему. - person Shelby Moore III; 18.09.2013
comment
Я согласен с @soc. contains не использует eq, он использует _2 _ / _ 3_. Довольны ли вы этим - отдельный разговор. - person nafg; 08.10.2013

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

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===)
person Kim Stebel    schedule 02.12.2011
comment
Это вообще не список сутенеров. Кроме того, пример ясно показывает, что === не работает со String с одной стороны и Int с другой. - person Kim Stebel; 03.12.2011
comment
Эта проблема была решена в Haskell 13 лет назад с помощью класса типов Eq. - person Lambda Fairy; 09.12.2011
comment
Нет, это намного лучше, потому что он по-прежнему поддерживает подтипы. - person Kim Stebel; 09.12.2011
comment
Ваш ответ неверен. List(new Sub) exists ((new Super)===) правильно не вызывает ошибки. Однако List(new Super) exists ((new Sub)===) не компилируется! Та же проблема, что я обнаружил в своем ответе, ср. типы Super и Sub в моем ответе. - person Shelby Moore III; 19.09.2013

Я думаю, вы неправильно понимаете решение Мартина, это не 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.

person Didier Dupont    schedule 02.12.2011
comment
Я объединил <: и :. Однако, если тип входного аргумента и A имеют общий супертип B из-за неявного подчинения, свидетельство Eq[B] позволяет компилировать List.contains на входном типе, который не может никогда содержаться в списке. - person Shelby Moore III; 03.12.2011
comment
Хорошо, я понимаю вашу точку зрения. Он основан на идее, что объект может равняться (Any.equals или Eq) только объекту того же типа. В общем случае это не так (см. Равенство коллекций). Ваш вопрос по-прежнему интересен, когда вы знаете, что это так. - person Didier Dupont; 03.12.2011
comment
Exact или подтип, наследующий тот же метод equals. Any.equals не обеспечивает соблюдение этого требования о выделении подтипов в целом, но, как правило, Eq будет, поэтому необходимо мой ответ. Невозможно обеспечить соблюдение этого требования о выделении подтипов не только для коллекций, но и для любого параметризованного типа из-за стирания? Разве implicit eql : Eq[B] не будет выбирать разные экземпляры для параметризации каждого типа в параметре B? Возможно, двухпараметрический Eq[A,B] подойдет лучше, чем предложение Мартина. Будет ли 1 == BigInt(1) поддерживаться неявным преобразованием Int => BitInt? - person Shelby Moore III; 04.12.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.

person Peter Schmitz    schedule 02.12.2011
comment
Ваш ответ неверен. Попробуйте его на типах Super и Sub, как я объясняю как в своем ответе, так и в комментарии под неправильным ответом Ким Стебель. - person Shelby Moore III; 18.09.2013

В примерах используется 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 или имеет неявно заменяемый супертип.]


ОБНОВЛЕНИЕ 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

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

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

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


Обновление: я добавил комментарий ниже ответа soc, поэтому теперь думаю, что его точка зрения не имеет отношения к делу. Равенство всегда должно основываться на подтипных отношениях, что, как известно, предлагает Мартин Одерски о новом пересмотре равенства (см. также его версию contains). Любая специальная полиморфная эквивалентность (например, BitInt(1) == 1) может быть обработана с помощью неявных преобразований. Я объяснил в своем комментарии ниже ответ Didierd, что без моего улучшения ниже, предложенный Мартином contains afaics будет иметь семантическую ошибку, в результате чего взаимно неявно вложенный супертип (отличный от 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 должно по-прежнему проверять соответствие конкретного типа ввода. Компилятор не может удалить эти накладные расходы.

person Community    schedule 02.12.2011
comment
Эффективность выполнения в значительной степени зависит от реализации equals ... независимо от того, сравниваете ли вы по типам или по значениям. - person soc; 03.12.2011
comment
Программа, отклоненная компилятором, не может повлиять на эффективность выполнения ... - person soc; 04.12.2011
comment
@soc Если компилятор отклоняет операцию, которая всегда возвращает false и которую в противном случае выполняла бы среда выполнения, эффективность выполнения была повышена. Мартин Одерски упомянул об этом. - person Shelby Moore III; 04.12.2011
comment
@soc Также предотвращаются недоступные ветви выражений. - person Shelby Moore III; 04.12.2011
comment
@soc Это не повышение эффективности компиляции. Эффективность программиста повышается за счет предотвращения таких случаев человеческой ошибки, что сокращает время выполнения. Я заменю неэффективность времени выполнения на семантическую неэффективность, которая ухудшает производительность времени выполнения. Спасибо. - person Shelby Moore III; 04.12.2011