user7938511
user7938511

Reputation: 167

Why/How does this scala contravariant example work?

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

Answers (2)

dk14
dk14

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

Alexey Romanov
Alexey Romanov

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 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 Apples, since they are Fruits! The only thing Scala knows about 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

Related Questions