简体   繁体   中英

Circumventing variance checks with extension methods

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?

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.

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:

  • No BitSet s for Int s only
  • No Ordering s
  • No custom hashing functions.

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 .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM