Reputation: 167
This code seems to set up a contravariant example: FruitBox can take in 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
}
That is all fine...but why does it work ? specifically: 1. When the FruitBox is initialized, why does the "type U >: T" not constrain it to supertypes of Fruit? In spite of that constraint, the FruitBox is able to put the subtypes if Fruit ( oranges and apples )...how?
Upvotes: 3
Views: 152
Reputation: 22374
To complement @AlexanderRomanov's answer Scala treats U >: T
as an existential type _ >: T
. Basically it materializes to T
when you actually specify it:
val orangeBox = new Box[Orange]
Btw, another more conventional way to do it is to implement U
in subclass:
trait Box[-T]{
type U >: T
}
class BoxImpl[T] extends Box[T]{
type U = T
}
So, if you used type parameter instead of type member, the code would be like:
class Box[T, _ >: T]
val orangeBox = new Box[Orange, Orange]
In your case Scala just finds appropriate type for you in order to "materialize" that existential.
The only inconsistency here is that it actually doesn't 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're instantiating your Box[Orange]
it ignores -T
as in class BoxImpl[T] extends Box[T]
(regardless that original Box
had -T
)
Upvotes: 0
Reputation: 170713
First, while Scala allows you to write new Box[Fruit]
, leaving U
an abstract member, I don't understand why. However, Scala seems to assume The only thing Scala knows about U = T
in this case. Since your code never implements U
, it can just be replaced by T
. So you end up with def put(t: Fruit)
in FruitBox
: of course it accepts Apple
s, since they are Fruit
s!U
is that it's a supertype of T
; thus T
is a subtype of U
, and so is every subtype of T
. So any subtype of Fruit
can be passed to FruitBox.put
. So def put(t: U): Unit
is effectively the same as put(t: T): Unit
unless you implement U
as in new Box[Fruit] { type U = Object }
.
a FruitBox that can put apples and oranges, and an OrangeBox that can only put oranges. That looks like contra-variant behavior, and I am fine with it.
It isn't at all contravariant behavior; you'll get exactly the same with
class Box[T] {
def put(t: T): Unit= {box += t}
val box = scala.collection.mutable.ListBuffer[T]()
}
Upvotes: 3