Reputation: 2582
This doesn't compile:
class MyClass[+A] {
def myMethod(a: A): A = a
}
//error: covariant type A occurs in contravariant position in type A of value a
Alright, fair enough. But this does compile:
class MyClass[+A]
implicit class MyImplicitClass[A](mc: MyClass[A]) {
def myMethod(a: A): A = a
}
Which lets us circumvent whatever problems the variance checks are giving us:
class MyClass[+A] {
def myMethod[B >: A](b: B): B = b //B >: A => B
}
implicit class MyImplicitClass[A](mc: MyClass[A]) {
def myExtensionMethod(a: A): A = mc.myMethod(a) //A => A!!
}
val foo = new MyClass[String]
//foo: MyClass[String] = MyClass@4c273e6c
foo.myExtensionMethod("Welp.")
//res0: String = Welp.
foo.myExtensionMethod(new Object())
//error: type mismatch
This feels like cheating. Should it be avoided? Or is there some legitimate reason why the compiler lets it slide?
Update:
Consider this for example:
class CovariantSet[+A] {
private def contains_[B >: A](b: B): Boolean = ???
}
object CovariantSet {
implicit class ImpCovSet[A](cs: CovariantSet[A]) {
def contains(a: A): Boolean = cs.contains_(a)
}
}
It certainly appears we've managed to achieve the impossible: a covariant "set" that still satisfies A => Boolean
. But if this is impossible, shouldn't the compiler disallow it?
Upvotes: 5
Views: 252
Reputation: 44918
You did not achieve the impossible. You just chose a trade-off that is different from that in the standard library.
What you lost
The signature
def contains[B >: A](b: B): Boolean
forces you to implement your covariant Set
in a way that works for Any
, because B
is completely unconstrained. That means:
BitSet
s for Int
s onlyOrdering
s This signature forces you to implement essentially a Set[Any]
.
What you gained
An easily circumventable facade:
val x: CovariantSet[Int] = ???
(x: CovariantSet[Any]).contains("stuff it cannot possibly contain")
compiles just fine. It means that your set x
, which has been constructed as a set of integers, and can therefore contain only integers, will be forced to invoke the method contains
at runtime to determine whether it contains a String
or not, despite the fact that it cannot possibly contain any String
s. So again, the type system doesn't help you in any way to eliminate such nonsensical queries which will always yield a false
.
Upvotes: 2
Reputation: 170733
I don't think it's cheating any more than the version after desugaring is:
val foo: MyClass[String] = ...
new MyImplicitClass(foo).myExtensionMethod("Welp.") // compiles
new MyImplicitClass(foo).myExtensionMethod(new Object()) // doesn't
The reason is that the type parameter on MyImplicitClass
constructor gets inferred before myExtensionMethod
is considered.
Initially I wanted to say it doesn't let you "circumvent whatever problems the variance checks are giving us", because the extension method needs to be expressed in terms of variance-legal methods, but this is wrong: it can be defined in the companion object and use private state.
The only problem I see is that it might be confusing for people modifying the code (not even reading it, since those won't see non-compiling code). I wouldn't expect it to be a big problem, but without trying in practice it's hard to be sure.
Upvotes: 3