Обход проверок на отклонения с помощью методов расширения

#scala #covariance #generic-variance

#scala #ковариация #generic-отклонение

Вопрос:

Это не компилируется:

 class MyClass[ A] {
  def myMethod(a: A): A = a
}
//error: covariant type A occurs in contravariant position in type A of value a
  

Хорошо, достаточно справедливо. Но это действительно компилируется:

 class MyClass[ A]

implicit class MyImplicitClass[A](mc: MyClass[A]) {
  def myMethod(a: A): A = a
}
  

Что позволяет нам обходить любые проблемы, возникающие при проверке отклонений:

 class MyClass[ A] {
  def myMethod[B >: A](b: B): B = b  //B >: A => B
}

implicit class MyImplicitClass[A](mc: MyClass[A]) {
  def myExtensionMethod(a: A): A = mc.myMethod(a)  //A => A!!
}

val foo = new MyClass[String]
//foo: MyClass[String] = MyClass@4c273e6c

foo.myExtensionMethod("Welp.")
//res0: String = Welp.

foo.myExtensionMethod(new Object())
//error: type mismatch
  

Это похоже на обман. Следует ли этого избегать? Или есть какая-то законная причина, по которой компилятор позволяет этому скользить?

Обновить:

Рассмотрим это, например:

 class CovariantSet[ A] {
  private def contains_[B >: A](b: B): Boolean = ???
}

object CovariantSet {
  implicit class ImpCovSet[A](cs: CovariantSet[A]) {
    def contains(a: A): Boolean = cs.contains_(a)
  }
}
  

Похоже, нам определенно удалось достичь невозможного: ковариантного «множества», которое все еще удовлетворяет A => Boolean . Но если это невозможно, не должен ли компилятор запретить это?

Ответ №1:

Я не думаю, что это обман больше, чем версия после десугаринга:

 val foo: MyClass[String] = ...
new MyImplicitClass(foo).myExtensionMethod("Welp.") // compiles
new MyImplicitClass(foo).myExtensionMethod(new Object()) // doesn't
  

Причина в том, что параметр типа в MyImplicitClass конструкторе выводится до myExtensionMethod рассмотрения.

Изначально я хотел сказать, что это не позволяет вам «обойти любые проблемы, которые дают нам проверки на отклонения», потому что метод расширения должен быть выражен в терминах методов, разрешающих отклонения, но это неправильно: он может быть определен в сопутствующем объекте и использовать частное состояние.

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

Комментарии:

1. Ну, без этого для меня нет ничего, что даже выглядело бы как обход проверок (как Андрей также говорит в своем комментарии). Если что, я бы добавил строку, показывающую, что foo.myExtensionMethod(new Object()) не компилируется, хотя foo.myMethod(new Object()) компилируется.

2. Думаю, я действительно выбросил ребенка вместе с водой из ванны. Восстановлено вместе с вашим дополнением. Спасибо.

Ответ №2:

Вы не достигли невозможного. Вы только что выбрали компромисс, отличный от того, что есть в стандартной библиотеке.

Что вы потеряли

Подпись

 def contains[B >: A](b: B): Boolean
  

заставляет вас реализовать ваш ковариант Set таким образом, который работает для Any , потому что B он абсолютно неограничен. Это означает:

  • Нет BitSet s только для Int s
  • Нет Ordering s
  • Нет пользовательских функций хеширования.

Эта подпись вынуждает вас реализовать по существу Set[Any] .

Что вы получили

Легко обходимый фасад:

 val x: CovariantSet[Int] = ???
(x: CovariantSet[Any]).contains("stuff it cannot possibly contain")
  

компилируется просто отлично. Это означает, что ваш set x , который был сконструирован как набор целых чисел и, следовательно, может содержать только целые числа, будет вынужден вызвать метод contains во время выполнения, чтобы определить, содержит ли он String или нет, несмотря на то, что он не может содержать никаких String s. Итак, система типов никоим образом не помогает вам устранить такие бессмысленные запросы, которые всегда будут выдавать false .

Комментарии:

1. Нет, вы можете сделать contains[B :> A] приватным. А затем получить к нему доступ через неявный класс.

2. @Lasf Ну, в private вы можете делать все, что хотите. В этом-то и суть private . Но если это частное, то у оболочки также нет к нему доступа.

3. Я добавил пример в конец своего вопроса.

4. @Lasf Я добавил пример, который показывает, что ваши подписи типов не помогают исключить запросы, которые всегда будут давать результат false .