Generics: Constraints

A generic type-constraint indicates a requirement that a type must fulfill in order to be accepted as a type argument for a given type parameter. (For example, it might have to be a given class type or a subtype of that class type, or it might have to implement a given interface.)

There are two kinds of generic type constraints, specified by the as and super keywords, respectively. Each is discussed below.

Specifying Constraints via as

Consider the following example in which class Complex has one type parameter, T, and that has a constraint, num:

<?hh

namespace Hack\UserDocumentation\Generics\Constraints\Examples\Constraint;


class Complex<T as num> {
  private T $real;
  private T $imag;
  public function __construct(T $real, T $imag) {
    $this->real = $real;
    $this->imag = $imag;
  }
  public static function add(Complex<T> $z1, Complex<T> $z2): Complex<num> {
    return new Complex($z1->real + $z2->real, $z1->imag + $z2->imag);
  }

  public function __toString(): string {
    if ($this->imag === 0.0) {
      // Make sure to cast the floating-point numbers to a string.
      return (string) $this->real;
    } else if ($this->real === 0.0) {
      return (string) $this->imag . 'i';
    } else {
      return (string) $this->real . ' + ' . (string) $this->imag . 'i';
    }
  }
}

function run(): void {
  $c1 = new Complex(10.5, 5.67);
  $c2 = new Complex(4, 5);
  // You can add one complex that takes a float and one that takes an int.
  echo "\$c1 + \$c2 = " . Complex::add($c1, $c2) . "\n";
  $c3 = new Complex(5, 6);
  $c4 = new Complex(9, 11);
  echo "\$c3 + \$c4 = " . Complex::add($c3, $c4) . "\n";
}

run();
Output
$c1 + $c2 = 14.5 + 10.67i
$c3 + $c4 = 14 + 17i

Without the as num constraint, a number of errors are reported, including the following:

  • The return statement in method add performs arithmetic on a value of unknown type T, yet arithmetic isn't defined for all possible type arguments.
  • The if statement in method __toString compares a value of unknown type T with a float, yet such a comparison isn't defined for all possible type arguments.
  • The return statement in method __toString negates a value of unknown type T, yet such an operation isn't defined for all possible type arguments. Similarly, a value of unknown type T is being concatenated with a string.

The run() code creates float and int instances, respectively, of class Complex.

In summary, T as U asserts that T must be a subtype of U.

Specifying Constraints via super

Unlike an as type constraint, T super U asserts that T must be a supertype of U.

This kind of constraint is rather exotic, but solves an interesting problem encountered when multiple types "collide". Here is an example of how it's used on method concat in the library interface type ConstVector:

interface ConstVector<+T> {
  public function concat<Tu super T>(ConstVector<Tu> $x): ConstVector<Tu>;
  // ...
}

Consider the case in which we call concat to concatenate a Vector<float> and a Vector<int>. As these have a common supertype, num, the super constraint allows the checker to determine that num is the inferred type of Tu.