0

I was reading through http://oldfashionedsoftware.com/2008/08/26/variance-basics-in-java-and-scala/

and am looking at the code

class CoVar[+T](param1: T) {
  def method1(param2: T) = { }
  def method2: T = { param1 }
  def method3: List[T] = { List[T](param1) }
  def method4[U >: T]: List[U] = { List[U](param1) }
  val val1: T = method2
  val val2: Any = param1
  var var1: T = method2
  var var2: Any = param1
}

then if I were to have a

val covar1 = new CoVar(new Car)
val covar2: CoVar[Vehicle] = covar1 //completely legal with covariant

Now, let's walk through the methods

  • method1 - I don't get why this doesn't compile and that is my main question
  • method2 - param1 is a car and method2 returns a Vehicle which is fine since Car is a Vehicle
  • method3 - since List[Vehicle] is returned and Car is a Vehicle this is fine
  • var1 - same question I believe and not too different

I would think this would be ok with method1(param: Vehicle) since I can pass in a new Vehicle just fine or a new Car just fine

but the original CoVar class does not compile since it says method1 is contravariant position. I thought contravariant would mean I could pass in a

Now, walking through this with ContraVar, and method1 again we have

class ContraVar[-T](param1: T) {
    def method1(param2: T) = { }
    val val2: Any = param1
    var var2: Any = param1
}

val temp1 = new ContraVar(new Car)
val temp2: ContraVar[Ford] = temp1

temp2.method1(new Ford)
temp2.method1(new FordMustang)
temp2.method1(new Car) //fails to compile(good)

which work just fine. Can someone please explain why method1 breaks on CoVar? Perhaps I am headed down the completely wrong path on what would go wrong with letting method1 compile just fine?

thanks, Dean

Dean Hiller
  • 19,235
  • 25
  • 129
  • 212

3 Answers3

2

Your question boils down to why the following is illegal

trait Tool[+A] {
  def treat(c: A): Unit
}

Just imagine it would compile… Let's look at a use case from the outside:

def apply[A](tool: Tool[A], car: A): Unit = tool.treat(car)

Say there are two possible types for A:

trait Car
trait Mustang extends Car { def awe(): Unit }

Covariance would mean Tool[Mustang] <: Tool[Car]. So whenever a Tool[Car] is asked for, you could use a Tool[Mustang]. Now imagine a Tool[Mustang]:

val tm = new Tool[Mustang] { def treat(m: Mustang) = m.awe() }

And now you would be able to call:

apply[Car](tm, new Car {})

This would mean, tm could access a non-existing method awe in a generic car. Obviously this is not a sound type relation. Therefore, whenever a type is used in argument position, it must be invariant or contravariant.

0__
  • 66,707
  • 21
  • 171
  • 266
  • hmmmm, so what if they had decided to only allow one to subclass Tool[+A] and Tool[Mustang] was not allowed to be subclassed. ie. is there another case where method1 breaks down or would that one language choice have prevented this issue? – Dean Hiller Sep 30 '14 at 00:26
  • I don't understand your question. You can create a subclass `SubTool[A] extends Tool[A]` where you decide that `A` becomes invariant. Then you can allow `A` to appear in method arguments of `SubTool`. – 0__ Sep 30 '14 at 10:11
  • oh, never mind, I get it......I think it might be better to add "trait SubTool extends Tool[Mustang] { def treat(m: Mustang) = m.awe() } ok, this makes complete sense to me now when I think about it that way since it is a typical java pattern. The other way you have it there confused me until I slept on the problem a bit and reformed it in terms of my java knowledge. – Dean Hiller Sep 30 '14 at 15:18
1

Let's give your method1 a body:

class CoVar[+T] {
    var listOfT: List[T] = Nil

    // method1 prepends the given element to listOfT
    def method1(param2: T) = { listOfT = param2 :: ListOfT }
}

Now we actually do something with the parameter passed into method1, it will be easy to see how something wrong can happen if this were allowed.

// Lets construct one of these that holds Ints. and add something to it
val covarInt = new CoVar[Int]
covarInt.method1(1)

// lets assign to a more general value, this is no problem because of the covariance
val covarAny: CoVar[Any] = covarInt

// now lets do a bad thing:
covarAny.method1("this is a string not an Int")

the last line shows the bad thing that would be allowed. Since covarAny is type CoVar[Any], that means that in the CoVar class, type T = Any, so the type expected as input to method1 is Any, so passing a String into this function should be allowed since String is an Any. However the method body would then try to prepend our passed String to a list of Ints which should not be allowed.

stew
  • 11,276
  • 36
  • 49
  • that doesn't seem so bad since val myNewList: Any param2 :: ListOfT is valid, right? I remember as you add to your list, type inference has the list become the highest type so that seems just fine and valid. – Dean Hiller Sep 30 '14 at 15:11
  • or in the scala repl, scala> val myList = 56 :: "hi there" :: Nil myList: List[Any] = List(56, hi there) is valid. – Dean Hiller Sep 30 '14 at 15:11
  • yes, but listOfT is not type List[Any] its type List[T]. think about having this: `def head: T = listOfT.head` – stew Sep 30 '14 at 15:43
  • as soon as you changed it to covarAny: CoVar[Any], then listOfT: List[T] is listOfT[Any] and method1 is method1(param2: Any)....or at least that is how I understand it. I think the other answer demonstrates a more straightforward break. – Dean Hiller Sep 30 '14 at 17:38
  • yes, but not from the perspective of covarInt, what happens when you call covarInt.head? a CoVar[Int] returns a String? – stew Sep 30 '14 at 23:00
0

I have a new answer that I thought helped me alot on why params are contravariant rather than covariant. This example can be done all without generics too.

Let's use the function objects instead and define an Animal, Bird, and Duck class

class Animal {
   def makeSound() = "animalsound"
   def walk()
}
class Bird extends Animal {
   def makeSound() = "tweeeeet"
   def fly()
}
class Duck extends Bird {
   def makeSound() = "quack"
   def paddle()
}

Now, let's try to define a covariant function

val doSomething: (Bird => String) = { d:Duck => d.paddle() }

Naturally, when a Bird is passed in, we can't cast to a Duck as it may not be a Duck and it certainly can't paddle with no paddle method in the Bird class.

Now, let's try to define a contravariant function

val doSomething: (Bird => String) = { a:Animal => a.walk() }

This now compiles and works because a bird is an Animal so it will have a walk method. This really cleared things up even more for me and naturally methods are very much like functions or you can always convert the method to a function in which case it needs to be contravariant for the params.

Now, is there one more way we could do something here as in T >: Bird maybe? I wonder if I were to do a ( T >: Bird => String ) = .....

well, I think I am beyond my knowledge there at this point and leave that as a TODO on my list of things to learn about.

Dean Hiller
  • 19,235
  • 25
  • 129
  • 212