Верхняя граница типа разрешает подтипы, но не родительский тип

Возможно ли иметь универсальный метод с привязкой к типу, который означает «каждый возможный конкретный подкласс этого признака, но не сам признак?»

В качестве примера предположим, что у меня есть следующая иерархия наследования:

sealed trait Fruit

case class Apple() extends Fruit
case class Orange() extends Fruit
...
case class Watermelon() extends Fruit

Я хочу определить метод def eatFruit[T <: ???](fruit: Seq[T]), который позволит T иметь тип Apple, Orange, Watermelon и т. д., но не тип Fruit. Тип, связанный с [T <: Fruit], очевидно, не выполняет эту работу.

Первоначальный стимул для этого заключается в том, что у нас есть класс FruitRepository, который позволяет вставлять разные фрукты пакетно/оптом. Пакетная обработка выполняется извне по отношению к классу, поэтому на данный момент он имеет множество методов типа saveApples(apples: Seq[Apple]), saveOranges(oranges: Seq[Orange]) и т. д., которые содержат много дублирующей логики, включающей создание оператора пакетного обновления. Я хотел бы управлять этим более общим способом, но любой метод saveFruit(fruit: Seq[Fruit]) позволит, например. список, содержащий как яблоки, так и апельсины, которые репозиторий не может обработать.

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


person Astrid    schedule 03.05.2018    source источник


Ответы (1)


Мы можем комбинировать директиву верхней границы с пользовательским неявным применением неравенства типов. Взято отсюда (или обычно см.: Применить различие типов):

@annotation.implicitNotFound(msg = "Cannot prove that ${A} =!= ${B}.")
trait =!=[A, B]
object =!= {
  class Impl[A, B]
  object Impl {
    implicit def neq[A, B] : A Impl B = null
    implicit def neqAmbig1[A] : A Impl A = null
    implicit def neqAmbig2[A] : A Impl A = null
  }

  implicit def foo[A, B](implicit e: A Impl B): A =!= B = null
}

И тогда делаем:

def eatFruit[T <: Fruit](implicit ev: T =!= Fruit) = ???

И когда мы называем это:

def main(args: Array[String]): Unit = {
  eatFruit[Fruit]
}

Мы получаем:

Error:(29, 13) Cannot prove that yuval.tests.FooBar.Fruit =!= yuval.tests.FooBar.Fruit.
    eatFruit[Fruit]

Но это компилируется:

eatFruit[Orange]

Вся магия здесь связана с созданием неоднозначности имплицитов в области видимости для пары [A, A], так что компилятор будет жаловаться.


Мы также можем пойти дальше и реализовать собственный логический тип, например, назовем его =<:=!=. Мы можем немного изменить предыдущую реализацию:

@annotation.implicitNotFound(msg = "Cannot prove that ${A} =<:=!= ${B}.")
trait =<:=!=[A,B]
object =<:=!= {
  class Impl[A, B]
  object Impl {
    implicit def subtypeneq[B, A <: B] : A Impl B = null
    implicit def subneqAmbig1[A] : A Impl A = null
    implicit def subneqAmbig2[A] : A Impl A = null
  }

  implicit def foo[A, B](implicit e: A Impl B): A =<:=!= B = null
}

И сейчас:

case class Blue()

def main(args: Array[String]): Unit = {
  eatFruit[Fruit] // Doesn't compile
  eatFruit[Blue] // Doesn't compile
  eatFruit[Orange] // Compiles
}
person Yuval Itzchakov    schedule 03.05.2018
comment
Хорошо, использование двусмысленных имплицитов для принудительного неравенства типов — это своего рода злой гений. Тем не менее, хотя это отвечает на мой вопрос, я хочу указать, что это работает для меня, только если я укажу универсальный тип - в моем примере, если я напишу eatFruit(List(Apple(), Orange()) и позволю компилятору разрешить универсальный тип, он все равно компилируется. Не уверен, почему - может быть, это как-то связано с тем фактом, что их общий супертип на самом деле Fruit with Product with Serializable? - person Astrid; 04.05.2018
comment
Только что проверил это - если у вас есть запечатанный трейт, где все наследуемые объекты являются классами case, вам также нужно добавить неявный параметр =!= для Fruit with Product with Serializable. - person Astrid; 04.05.2018