Generics: Unresolved

Imagine the following usage of a generic Box class

<?hh

namespace Hack\UserDocumentation\Generics\Unresolved\Examples\Unresolved;

class Box<T> {
  private array<T> $contents;
  public function __construct() {
    $this->contents = array();
  }
  public function addTo(T $item) {
    $this->contents[] = $item;
  }
  public function get(): array<T> {
    return $this->contents;
  }
}

function add_box_of_ints(Box<int> $box): int {
  return array_sum($box->get());
}

function unresolved(): void {
  $box = new Box();
  // You might think that T has been bound to int, but no.
  $box->addTo(4);
  // Now we are unresolved. The typechecker knows we are using Box as a
  // container of ints and now strings. Do we have a mixed Box?
  $box->addTo('Hi');
  // Well, we are not at a boundary, so the typechecker just let's this go.
}

function resolved(): void {
  $box = new Box();
  // You might think that T has been bound to int, but no.
  $box->addTo(4);
  // Now we are unresolved. The typechecker knows we are using Box as a
  // container of ints and now strings. Do we have a mixed container?
  $box->addTo('Hi');
  // still unresolved
  $box->addTo(99);
  // Here we are resolved! add_box_of_ints is expecting a Box<int> and we
  // don't have it. Now the typechecker can issue an error about adding the
  // string
  var_dump(add_box_of_ints($box));
}

function run(): void {
  unresolved();
  resolved();
}

run();
Output
Catchable fatal error: Value returned from function Hack\UserDocumentation\Generics\Unresolved\Examples\Unresolved\add_box_of_ints() must be of type int, float given in /data/users/joelm/user-documentation/guides/hack/40-generics/06-unresolved-examples/unresolved.php.type-errors on line 19

We create a new Box. Store an int. Then store a string.

Intuitively, you would think that the the typechecker should raise an error. But it doesn't!

At the point where we store string in unresolved(), we have an unresolved type (see discussion on non-generic unresolved types here). This means that our Box could be a Box<int> or a Box<string>. We just don't know yet. And since we never hit a boundary relating to the usage of Box in unresolved(), the typechecker just moves on.

The typechecker generally works on the boundaries. It checks against method calls to methods with annotated parameters. It checks when we return from a function against the type annotation of the return type. And so on.

When we store the string in resolved(), we are not yet at the boundary condition. It is only when we call a function expecting a Box<int> where the type error would be thrown.