I'm puzzled why scala treats def
and type
differently when counting variance constraints?
I tried to break my wall of text and factored my return type into a separate type declaration, and got a variance error immediately. Here is a minimized example:
class Bad[+A] {
type Check[B >: A] = Bad[B]
def check[B >: A] : Check[B] = this
}
which fails:
covariant.scala:2: error: covariant type A occurs in invariant position in type >: A of type B
type Check[B >: A] = Bad[B]
^
one error found
Still, it works fine without the extra type declaration:
class Good[+A] {
def check[B >: A]: Good[B] = this
}
class Ugly[+A] {
type Check[B >: A @scala.annotation.unchecked.uncheckedVariance] = Ugly[B]
def check[B >: A] : Check[B] = this
}
This is because as soon as the type member Check
escapes to the outside,
it can immediately appear both in co- and contravariant positions in functions and
methods:
class Bad[+A] {
type Check[B]
}
class B
class A extends B
val bad: Bad[A] = ???
import bad._
def hypotheticalMethodSomewhereOutside1(c: Check[B]) = ???
def hypotheticalMethodSomewhereOutside2(i: Int): Check[B] = ???
The crucial difference to the [B >: A]
type argument in the method check
def check[B >: A]: Good[B] = this
is that the single method check
is under your control, you can guarantee that
it does not use A
in a not-covariant position.
In contrast to that, the type member Check
can appear in infinitely many other methods
in both co- and contravariant positions, and those methods are not under your control,
so you cannot prohibit the usage of Check
in positions in which A
appears not-covariant,
e.g. you cannot prevent hypotheticalMethodSomewhereOutsideN
from the example above to
be defined by someone else.
Note that the presence of the method check
is not necessary for your Bad
example to fail:
// error: covariant type A occurs in invariant position in type >: A of type B
class Bad[+A] {
type Check[B >: A] = Bad[B]
// def check[B >: A] : Check[B] = this
}
However, if you hide the member type Check
from everyone
(really everyone, including even other instances of same class, that is,
with the extremely reclusive private[this]
access modifier):
class Bad[+A] {
private[this] type Check[B >: A] = Bad[B]
}
it compiles just nicely.
The private[this]
solves the problem, because with this modifier, only the methods
inside the class have to be inspected, no hypothetical methods somewhere outside
have to be taken into consideration:
class Ok[+A] {
private[this] type Check[B >: A] = Ok[B]
def ok[B >: A](c: Check[B]): Unit = ???
def alsoOk[B >: A]: Check[B] = ???
}
Note that the above can be written just as
class Good[+A] {
type Check[+B] = Good[B]
def ok[B >: A](c: Check[B]): Unit = ???
def alsoOk[B >: A]: Check[B] = ???
}
which makes sense only if the type constructor Good
has way more arguments than Check
.