Reputation: 848
I have a confusion in understanding covariance type being restricted in method parameters. I read through many materials and I am unable to get them the below concept.
class SomeThing[+T] {
def method(a:T) = {...} <-- produces error
}
In the above piece of code, a is of type T. Why can we not pass subtypes of T? All the expectations of method on parameter x, can be fulfilled by subtype of T perfectly.
Similarly when we have contravariant type T (-T), it can not be passed as method argument; but it is allowed. Why I think it can not be passed is: for e.g, say method invokes a method (present in object a) on a which is present in T. When we pass super type of T, it may NOT be present. But it is allowed by compiler. This confuses me.
class SomeThing[-T] {
def method(a:T) = {...} <-- allowed
}
So by looking at the above, it is covariant that should be allowed in method arguments as well as in the return type. Contravariant can not be applied.
Can someone please help me to understand.
Upvotes: 1
Views: 1648
Reputation: 51271
In the case of class SomeThing[T]
, placing a +
or -
before the T
actually effects the class itself more than the type parameter.
Consider the following:
val instanceA = new SomeThing[A]
val instanceB = new SomeThing[B]
If SomeThing
is invariant on T
(no +
or -
) then the instances will have no variance relationship.
If SomeThing
is covariant on T
([+T]
) then the instances will have the same variance relationship as A
and B
have. In other words, if A
is a sub-type of B
(or vice versa) then the instances will reflect that same relationship.
If SomeThing
is contravariant on T
([-T]
) then the instances will have the opposite variance relationship as A
and B
have. In other words, if A
is a sub-type of B
then instanceB
will be a sub-type of instanceA
.
But the variance indicator does effect how the type parameter can be used. If T
is marked +
then it can't be placed in a contravariant position and, likewise, if marked -
then it can't be placed in a covariant position. We bump up against this most often when defining methods.
Scala methods are very closely related to the Scala function traits: Function0
, Function1
, Function2
, etc.
Consider the definition of Function1
:
trait Function1[-T1, +R] extends AnyRef
Now let's say you want to pass a function of this type around.
def useThisFunc(f: A => B):Unit = {...}
Because a Function1
is contravariant on its received parameter and covariant on its result, all of the following are acceptable as a useThisFunc()
parameter.
val a2b : A => B = ???
val supa2b : SuperOfA => B = ???
val a2subb : A => SubOfB = ???
val supa2subb : SuperOfA => SubOfB = ???
So, in conclusion, if SomeThing
is covariant on T
then you can't have T
as a passed parameter of a member method because FunctionX
is contravariant on its parameter types. Likewise, if SomeThing
is contravariant on T
the you can't have T
as member method return type because FunctionX
is covariant on its return type.
Upvotes: 1
Reputation: 4595
I think you are attacking the problem backwards. The fact that you can't have a:T
as an argument of a method if T
is covariant comes as a constraint because otherwise some illogical code would be completely valid
class A
class B extends A
class C extends B
val myBThing = new SomeThing[B]
Here, myBThing.method
accepts a B
, and you are right that we can pass it anything that extends B
, so myBThing.method(new C)
is completely fine. However, myBThing.method(new A)
isn't!
Now, since we've defined SomeThing
with a covariant, I can also write this
val myAThing: SomeThing[A] = myBThing // Valid since B <: A entails SomeThing[B] <: Something[A] by definition of covariance
myAThing.method(new A) // What? You're managing to send an A to a method that was implemented to receives B and subtypes!
You can see now why we impose the constraint of not passing T
as a parameter (parameters are in a "contravariant position").
We can make a similar argument for contravariance in the return position. Remember that contravariance means B <: A
entails ``SomeThing[A] <: Something[B]`.
Assume you're defining the following
class A
class B extends A
class SomeThingA[-T](val value: T) // Compiler won't like T in a return type like myThing.value
// If the class definition compiled, we could write
val myThingA: SomeThing[A] = new SomeThing(new A)
val someA: A = myThingA.value
val myThingB: SomeThing[B] = myThingA // Valid because T contravariant
val someB: B = myThingB.value // What? I only ever stored an A!
For more details, see this answer.
Upvotes: 1
Reputation: 27421
The key thing about variance is that it affects how the class looks from the outside.
Covariance says that an instance of SomeThing[Int]
can be treated as an instance of SomeThing[AnyVal]
because AnyVal
is a superclass of Int
.
In this case your method
def method(a: Int)
would become
def method(a: AnyVal)
This is clearly a problem because you can now pass a Double
to a method of SomeThing[Int]
that should only accept Int
values. Remember that the actual object does not change, only the way that it is perceived by the type system.
Contravariance says that SomeThing[AnyVal]
can be treated as SomeThing[Int]
so
def method(a: AnyVal)
becomes
def method(a: Int)
This is OK because you can always pass an Int
where AnyVal
is required.
If you follow through the logic for return types you will see that it works the other way round. It is OK to return covariant types because they can always be treated as being of the superclass type. You can't return contravariant types because the return type might be a subtype of the actual type, which cannot be guaranteed.
Upvotes: 2