scala contravariant position on method

2019-09-06 12:15发布

问题:

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

回答1:

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.



回答2:

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.



回答3:

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.