3

Is it possible to have a generic method with an type bound that amounts to "every possible concrete subclass of this trait, but not the trait itself?"

As an example, suppose I have the following inheritance hierarchy:

sealed trait Fruit

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

I want to define a method def eatFruit[T <: ???](fruit: Seq[T]) that will allow T to be of type Apple, Orange, Watermelon, etc. but not of type Fruit. The type bound [T <: Fruit] obviously doesn't do the job.

The original impetus for this is that we have a FruitRepository class that allows batched/bulk inserts of different fruits. The batching is done externally to the class, so at the moment it has a lot of methods along the lines of saveApples(apples: Seq[Apple]), saveOranges(oranges: Seq[Orange]), etc. that contain a lot of duplicate logic involving the creation of a batch update statement. I'd like to manage this in a more generic way, but any method saveFruit(fruit: Seq[Fruit]) would allow for e.g. a list containing both apples and oranges, which the repository can't handle.

...I'll also admit that I'm now generally curious as to whether this sort of type bound is possible, even if we end up solving the repository problem in a different way.

Astrid
  • 1,808
  • 12
  • 24

1 Answers1

3

We can combine the upper bound directive with a custom implicit enforcement of type inequality. Taken from here (or generally see: Enforce type difference):

@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
}

And then we do:

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

And when we call it:

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

We get:

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

But this compiles:

eatFruit[Orange]

All the magic here is due to creating ambiguity of implicits in scope for the pair [A, A] such that the compiler will complain.


We can also take this one step further, and implement our own logical type, for example, let's call it =<:=!=. We can change the previous implementation a bit:

@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
}

And now:

case class Blue()

def main(args: Array[String]): Unit = {
  eatFruit[Fruit] // Doesn't compile
  eatFruit[Blue] // Doesn't compile
  eatFruit[Orange] // Compiles
}
Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
  • All right, using ambiguous implicits to force type inequality is some form of evil genius. That said, although this answers my question I want to point out this only works for me if I specify the generic type - in my example, if I write `eatFruit(List(Apple(), Orange())` and let the compiler resolve the generic type, it still compiles. Not sure why - maybe something to do with the fact that the common supertype of the two is really `Fruit with Product with Serializable`? – Astrid May 04 '18 at 06:25
  • 1
    Just verified this - if you have a sealed trait where all inheriting objects are case classes, you also need to add an implicit `=!=` parameter for `Fruit with Product with Serializable`. – Astrid May 04 '18 at 06:35