Generics: Subtypes

An area that may seem counter-intuitive is generics as it pertains to subtyping.

<?hh

namespace Hack\UserDocumentation\Generics\Subtypes\Examples\Intuitive;

function echo_add(num $x, num $y): void {
  echo $x + $y;
}

function get_int(): int {
  return rand();
}

function run(): void {
  $num1 = get_int();
  $num2 = get_int();
  echo_add($num1, $num2); // int is a subtype of num
}

run();
Output
2229204202

Since int is a subtype of num, the typechecker is perfectly fine with passing an int to a function that takes num. It would be fine if you pass a float to the function. It would also be fine if you pass one int and one float to the function.

However, do you think the typechecker should accept this?

<?hh

namespace Hack\UserDocumentation\Generics\Subtypes\Examples\CounterIntuitive;

class Box<T> {
  private Vector $box;
  public function __construct(int $firstItem) {
    $this->box = Vector {$firstItem};
  }
  public function add(T $v) {
    $this->box[] = $v;
  }
}

function addRandomToBox(Box<num> $x): void {
  $x->add(rand());
}

function createBox(): Box<int> {
  return new Box(3);
}

function run(): void {
  $box = createBox(); // we have a Box<int>
  addRandomToBox($box); // typechecker cannot guarantee a Box<int> now.
  var_dump($box); // HHVM doesn't care since we erase generics anyway.
}

run();
Output
object(Hack\UserDocumentation\Generics\Subtypes\Examples\CounterIntuitive\Box)#2 (1) {
  ["box":"Hack\UserDocumentation\Generics\Subtypes\Examples\CounterIntuitive\Box":private]=>
  object(HH\Vector)#1 (2) {
    [0]=>
    int(3)
    [1]=>
    int(123434323)
  }
}

It seems like it should be valid to pass an Box<int> to a function expecting a Box<num> since int is a subtype of num. However, in the case of generics, the subtyping relationship does not coincide with its primitive type counterparts.

The reason is that since your generic object is passed by reference, the typechecker has no way of safely knowing whether or not you are modifying the Box in addRandomToBox() to contain something that is not a num. While obvious to us that we are adding an int within addRandomToBox(), the typechecker does not actually consider what is happening. So it is unsure that what is returned to us is still a Box<int>.

The HHVM runtime doesn't care since we erase generics at runtime anyway.

Immutable Collections and Arrays

Since immutable collections cannot be changed and arrays are passed-by-value (i.e., a copy instead of reference), generics with the subtype relationships discussed above will actually pass the typechecker. This is because the typechecker can guarantee that the entity will not be changed when returned to the caller.

<?hh

namespace Hack\UserDocumentation\Generics\Subtypes\Examples\Immutable;

function addRandomToArray(array<num> $x): void {
  $x[] = 3.2; // this is a copy, not a reference
}

function createArray(): array<int> {
  return array(3);
}

function run(): void {
  $arr = createArray(); // we have a array<int>
  // typechecker CAN guarantee array<int> now since what is received by
  // addRandomToArray() is a copy (passed-by-value)
  addRandomToArray($arr);
  var_dump($arr); // Still only going to contain 3, not the 3.2.
}

run();
Output
array(1) {
  [0]=>
  int(3)
}