5

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?

Lasf
  • 2,536
  • 1
  • 16
  • 35

2 Answers2

3

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.

Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487
  • Well, without it there's nothing which even looks like circumventing the checks to me (as Andrey says in his comment as well). If anything, I'd add a line showing `foo.myExtensionMethod(new Object())` doesn't compile, while `foo.myMethod(new Object())` does. – Alexey Romanov Apr 01 '19 at 14:32
  • I guess I did throw the baby out with the bathwater there. Restored along with your addition. Thanks. – Lasf Apr 01 '19 at 14:42
2

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 BitSets for Ints only
  • No Orderings
  • 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 Strings. So again, the type system doesn't help you in any way to eliminate such nonsensical queries which will always yield a false.

Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
  • No, you can make the `contains[B :> A]` private. And then access it via the implicit class. – Lasf Apr 01 '19 at 15:26
  • @Lasf Well, in `private`, you can do whatever you want. That's exactly the point of `private`. But if it's private, then the wrapper doesn't have access to it either. – Andrey Tyukin Apr 01 '19 at 15:27
  • I added an example to the bottom of my question. – Lasf Apr 01 '19 at 15:29
  • @Lasf I added an example that shows that your type signatures don't help to rule out queries that will always yield `false`. – Andrey Tyukin Apr 01 '19 at 15:42