Privacy violation

[subtyping] [variance]

With declaration-site variance (see Covariant containers), a generic class can be declared to be covariant in its type parameter, as long as the type parameter is only used in output positions:

class Box<+X> {
  // allowed, X is an output (covariant)
  public X get() { ... }

  // disallowed, X is an input (contravariant)
  public void set(X x) { ... }
}

Since variance checking is verifying subtyping, a property of the public interface to a class, it should in principle be fine to allow methods that use X contravariantly (like set above), provided that they are marked private and therefore do not form part of the interface.

Whether or not this is sound hinges on exactly what the private modifier means: is it private to the object or private to the class? Conventionally, many object-oriented languages choose class-private, allowing access to private fields of an object of class A from any method of class A, regardless of whether the object being accessed was this or any other.

Choosing class-private makes the private variance check unsound, as pointed out by Emir et al.1 in their paper introducing declaration-site variance for C#. (The same issue later reappeared in Hack2).

// Counterexample by Emir et al.
class Bad<+X> {
  private X item;
  public void BadAccess(Bad<string> bs) {
    Bad<object> bo = bs;
    bo.item = new Button(); // we just wrote a button as a string
  }
}
// Counterexample by Andrew Kennedy
class Box<+T> {
  // OK, we've got a private field whose type involves the covariant T
  public function __construct(private T $elem) {
  }

  // As usual, a (safe) getter method
  public function get(): T { return $this->elem; }

  // Private gives us access to arbitrary instances of Box, even in static
  // methods. Note the use of covariant subtyping to put a string in
  // a Box<mixed>
  public static function updateAsString(Box<mixed> $x, string $s) : void {
    $x->elem = $s;
  }

  // We can now use this method to overwrite an integer with a string
  // but return it as an integer
  public static function morphIntToString(int $i) : int {
    $x = new Box($i);
    Box::updateAsString($x, 'hey you turned me into a string');
    return $x->get();
  }

  // Actually do it
  public static function useBox(): void {
    $i = Box::morphIntToString(23);
    echo('this should be an integer: ' . $i);
  }
}

Scala supports both private (class-private) and private[this] (object-private), with different variance checking. It also supports protected[this], an "object-protected" qualifier in which a field is accessible only via this, but both by the class itself and its subclasses.

Scala's protected[this] has the same variance-checking as private[this], allowing non-covariant uses of a type parameter in a protected[this] field, even from within a class marked as covariant. This is an extraordinarily subtle feature, and Scala's current implementation has a number of soundness issues.

First, Scala supports a form of multiple inheritance via "traits". A class can inherit from a covariant trait in multiple ways, and the two copies of the trait's interface are merged. This merging can be justified by covariance, but protected[this] allows classes to provide non-covariant features to subclasses. This is unsound, as different traits in the inheritance heirarchy can see different values of the type parameter in non-covariant ways3:

// Counterexample by Jason Zaugg
trait A[+X] {
  protected[this] def f(x: X): X = x
}

trait B extends A[B] {
  def kaboom = f(new B {})
}

// protected[this] disables variance checking
// of the signature of `f`.
//
// C's parent list unifies A[B] with A[C]
//
// The protected[this] loophole is widely used
// in the collections, every newBuilder method
// would fail variance checking otherwise.
class C extends B with A[C] {
  override protected[this] def f(c: C) = c
}

// java.lang.ClassCastException: B$$anon$1 cannot be cast to C
//  at C.f(<console>:15)
new C().kaboom

Second, Scala allows protected[this] to apply also to abstract type members, which can be exposed by a subclass4:

// Counterexample by Derek Lam
abstract class Base[+T] {
  protected[this] type TSeq <: MutableList[T]
  val v: TSeq
}

class Sub extends Base[Int] {
  type TSeq = MutableList[Int]
  val v = MutableList(42)
}

val x = new Sub()
(x: Base[Any]).v += "string!"
x.v.last + 42
// java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
1

Variance and Generalized Constraints for C# Generics (ECOOP '06), Burak Emir, Andrew Kennedy, Claudio Russo, Dachuan Yu (2006)