Why / How does this contravariant scala example work?
This code seems to create a contravariant example: FruitBox can accept apples or oranges.
class Fruit(name: String) { }
case class Apple(name: String) extends Fruit(name) {}
case class Orange(name: String) extends Fruit(name) {}
class Box[-T] {
type U >: T
def put(t: U): Unit= {box += t}
val box = scala.collection.mutable.ListBuffer[U]()
}
object test {
val FruitBox = new Box[Fruit]
// Fruit Box takes in everything
FruitBox.put(new Fruit("f1"))
FruitBox.put(new Orange("o1"))
FruitBox.put(new Apple("a1"))
// and Orange Box takes in only oranges
val OrangeBox = new Box[Orange]
//OrangeBox.put(new Apple("o2") ==> Compile Error that makes sense
OrangeBox.put(new Orange("o2"))
// Contra-variant nature is also demonstrated by
val o: Box[Orange] = FruitBox
}
It's okay ... but why does it work? in particular: 1. When FruitBox is initialized, why doesn't "type U>: T" restrict it to FruitBox supertypes? Despite this limitation, FruitBox can supply subtypes if Fruit (oranges and apples) ... how?
source to share
First, while Scala allows you to write new Box[Fruit]
while leaving an U
abstract member, I don't understand why. However, Scala seems to be accepting The only thing Scala knows about U = T
in this case. Since your code never implements U
, you can simply replace it with T
. So, you get to def put(t: Fruit)
in FruitBox
: Of course, it takes Apple
s, as they Fruit
s!U
is that it is a supertype of T
; thus, T
is a subtype U
, and each subtype is T
. Therefore, any subtype Fruit
can be passed to FruitBox.put
. So, is def put(t: U): Unit
actually the same as put(t: T): Unit
if you don't implement U
as in new Box[Fruit] { type U = Object }
.
FruitBox, which can hold apples and oranges, and OrangeBox, which can only hold oranges. This looks like conflicting behavior and I'm fine with it.
This is not at all contravariant behavior; you get the same with
class Box[T] {
def put(t: T): Unit= {box += t}
val box = scala.collection.mutable.ListBuffer[T]()
}
source to share
In addition to @AlexanderRomanov's answer, Scala treats it U >: T
as an existential type _ >: T
. It basically materializes before T
when you actually specify it:
val orangeBox = new Box[Orange]
Btw, another common way to do this is to implement U
in a subclass:
trait Box[-T]{
type U >: T
}
class BoxImpl[T] extends Box[T]{
type U = T
}
So, if you used a type parameter instead of a type member, the code would look like this:
class Box[T, _ >: T]
val orangeBox = new Box[Orange, Orange]
In your case, Scala simply finds a suitable type for you to "materialize" this existential.
The only inconsistency here is that it doesn't actually allow:
class BoxImpl[-T] extends Box[T]{
type U = T
}
//error: contravariant type T occurs in invariant position in type T of type U
So basically, when you create yours Box[Orange]
, it ignores -T
as in class BoxImpl[T] extends Box[T]
(whatever the original Box
had -T
)
source to share