Generics: Variance

Hack supports both generic covariance and contravariance. This is a fairly advanced topic, so we will not go into much detail; we will cover enough about the basics.

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 of covariance:

<?hh

namespace Hack\UserDocumentation\Generics\Variance\Examples\Covariance;

// 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(array<Animal> $p1): void { var_dump($p1); }

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

  g(array(new Animal(), new Animal()));
  g(array(new Cat(), new Cat(), new Animal())); // arrays are covariant
}

run();
Output
object(Hack\UserDocumentation\Generics\Variance\Examples\Covariance\C)#1 (1) {
  ["t":"Hack\UserDocumentation\Generics\Variance\Examples\Covariance\C":private]=>
  object(Hack\UserDocumentation\Generics\Variance\Examples\Covariance\Animal)#2 (0) {
  }
}
object(Hack\UserDocumentation\Generics\Variance\Examples\Covariance\C)#1 (1) {
  ["t":"Hack\UserDocumentation\Generics\Variance\Examples\Covariance\C":private]=>
  object(Hack\UserDocumentation\Generics\Variance\Examples\Covariance\Cat)#2 (0) {
  }
}
array(2) {
  [0]=>
  object(Hack\UserDocumentation\Generics\Variance\Examples\Covariance\Animal)#1 (0) {
  }
  [1]=>
  object(Hack\UserDocumentation\Generics\Variance\Examples\Covariance\Animal)#2 (0) {
  }
}
array(3) {
  [0]=>
  object(Hack\UserDocumentation\Generics\Variance\Examples\Covariance\Cat)#2 (0) {
  }
  [1]=>
  object(Hack\UserDocumentation\Generics\Variance\Examples\Covariance\Cat)#3 (0) {
  }
  [2]=>
  object(Hack\UserDocumentation\Generics\Variance\Examples\Covariance\Animal)#4 (0) {
  }
}

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.

Contravariant

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 of contravariance:

<?hh

namespace Hack\UserDocumentation\Generics\Variance\Examples\Contravariance;

// 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 setMe(T $val): void {
    $this->t = $val;
  }
}

class Animal {}
class Cat extends Animal {}

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

main();
Output
object(Hack\UserDocumentation\Generics\Variance\Examples\Contravariance\C)#3 (1) {
  ["t":"Hack\UserDocumentation\Generics\Variance\Examples\Contravariance\C":private]=>
  object(Hack\UserDocumentation\Generics\Variance\Examples\Contravariance\Animal)#1 (0) {
  }
}

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.