Generics: Variance

Hack supports both generic covariance and contravariance on a type parameter.

Each generic parameter can optionally be marked separately with a variance indicator:

  • + for covariance
  • - for contravariance

If no variance is indicated, the parameter is invariant.

Covariance

If Foo<int> is a subtype of Foo<num>, then Foo is covariant on T. 'co' means 'with'; and the subtype relationship of the generic type goes with the subtype relationship of arguments to a covariant type parameter. Here is an example:

// This class is readonly. Had we put in a setter for $this->t, we could not
// use covariance. e.g., if we had function setMe(T $x), you would get this
// cov.php:9:25,25: Illegal usage of a covariant type parameter (Typing[4120])
//   cov.php:7:10,10: This is where the parameter was declared as covariant (+)
//   cov.php:9:25,25: Function parameters are contravariant
class C<+T> {
  public function __construct(private T $t) {}
}

class Animal {}
class Cat extends Animal {}

function f(C<Animal> $p1): void {
  \var_dump($p1);
}

function g(varray<Animal> $p1): void {
  \var_dump($p1);
}

<<__EntryPoint>>
function run(): void {
  f(new C(new Animal()));
  f(new C(new Cat())); // accepted

  g(vec[new Animal(), new Animal()]);
  g(vec[new Cat(), new Cat(), new Animal()]); // arrays are covariant
}

A covariant type parameter is for read-only types. Thus, if the type can somehow be set, then you cannot use covariance.

Covariance cannot be used as the type of a parameter on any method, or as the type of any mutable property, in that class.

Contravariance

If Foo<num> is a subtype of Foo<int>, then Foo is contravariant on T. 'contra' means 'against'; and the subtype relationship of the generic type goes against the subtype relationship of arguments to a contravariant type parameter. Here is an example:

// This class is write only. Had we put in a getter for $this->t, we could not
// use contravariance. e.g., if we had function getMe(T $x): T, you would get
// con.php:10:28,28: Illegal usage of a contravariant type
//                   parameter (Typing[4121])
//  con.php:5:10,10: This is where the parameter was declared as
//                   contravariant (-)
//  con.php:10:28,28: Function return types are covariant
class C<-T> {
  public function __construct(private T $t) {}
  public function set_me(T $val): void {
    $this->t = $val;
  }
}

class Animal {}
class Cat extends Animal {}

<<__EntryPoint>>
function main(): void {
  $animal = new Animal();
  $cat = new Cat();
  $c = new C($cat);
  // calling set_me with Animal on an instance of C that was initialized with Cat
  $c->set_me($animal);
  \var_dump($c);
}

A contravariant type parameter is for write-only types. Thus, if the type can somehow be read, then you cannot use contravariant. (e.g., serialization functions are a good use case).

A contravariant type parameter cannot be used as the return type of any method in that class.

Was This Page Useful?
Thank You!
Thank You! If you'd like to share more feedback, please file an issue.