Types: Refining

Refining a type basically establishes that a value of one type is also of another type.

Suppose you want to take a given type and refine that particular type to another, compatible type. Hack allows this through the use of three constructs in control-flow situations:

  • checking for null
  • type-querying (e.g., via is_float())
  • using instanceof

Nullable to Non-Nullable

Remember that a nullable type allows the value of a variable to be of its type or null. There are times when you want to utilize only the non-null part of that type. You can refine the nullable type with null checks.

<?hh

namespace Hack\UserDocumentation\Types\Refining\Examples\Nullable;

function foo(?int $x): int {
  $a = 4;
  if ($x !== null) { // refine $x to just an int by verifying it is not null
    return $x + $a; // guaranteed that $x is not null now
  }
  return $a;
}

var_dump(foo(5));
Output
int(9)

Mixed to Primitive

Remember that mixed represents any annotatable type (except a nullable type). mixed can be refined into a more specific primitive type through the use of built-in type querying functions such as is_int(), is_float(), is_string(), etc.

<?hh

namespace Hack\UserDocumentation\Types\Refining\Examples\Mixed;

function foo(mixed $x): int {
  $a = 4;
  if (is_int($x)) { // refine $x to int by checking to see if $x is an int
    return $x + $a;
  } else if (is_bool($x)) {
    return (int) $x + $a; // know it is a bool, so can do safe cast
  }
  return $a;
}

var_dump(foo(true));
Output
int(5)

Object Instance Checks

Sometimes you want to know whether an object is a child of a parent class or implements a particular interface. You can use the instanceof check to help make this determination.

<?hh

namespace Hack\UserDocumentation\Types\Refining\Examples\Obj;

interface I {
  public function foo(): string;
}

class Base implements I {
  public function foo(): string {
    return "Base";
  }
}

class Child extends Base {
  // The __Override attribute is discussed in the section on attributes
  // TODO: LINK HERE WHEN READY!
  <<__Override>>
  public function foo(): string {
    return "Child";
  }
}

function bar(Base $b): Child {
  if ($b instanceof Child) { // refine $b to Child, a subclass of Base
    echo $b->foo(); // "Child"
    return $b;
  }
  echo $b->foo(); // "Base"
  return new Child();
}

function baz(I $i): Child {
  // guarantee that the interface will be a Child
  invariant($i instanceof Child, "Not Child");
  echo $i->foo(); // "Child"
  return $i;
}

function refine_object(): void {
  $c = new Child();
  bar($c);
  bar(new Base());
  baz($c);
}

refine_object();
Output
ChildBaseChild

instanceof Gotcha

Take a look at this example.

<?hh

namespace Hack\UserDocumentation\Types\Refining\Examples\Unresolved;

interface I {
  public function i_method(): bool;
}

abstract class Base {
  abstract public function foo(): string;
}

class Child1 extends Base implements I {
  <<__Override>>
  public function foo(): string {
    return "Child1";
  }
  public function i_method(): bool {
    return true;
  }
}

class Child2 extends Base {
  <<__Override>>
  public function foo(): string {
    return "Child2";
  }
}

function bar(Base $b): void {
  if ($b instanceof I) { // refine $b to interface I, but makes $b unresolved
    echo $b->i_method();
  }
  // This is a type error!
  // Given the instanceof check above, we have now made $b unresolved, a union
  // between a type of I and Base. So we can only call methods common to both.
  // which in this case there are none.
  echo $b->foo();
}

function unresolved(): void {
  $c = new Child1();
  bar($c);
}

unresolved();
Output
1Child1

Even though bar() was passed in a Base, and all children of Base implement foo(), once the instanceof check was done on $b, and the check returned true, the typechecker has to assume that $b is now an unresolved type of both Base and I. And since not all implementers of I have to be in the hierarchy of Base, we cannot guarantee that foo() is available any longer.

Invariant

There is also a special function:

invariant(<bool expression of fact>, "Message if not")

that can be used outside control-flow situation. It is essentially an assert to the typechecker that what you claim in the boolean statement is fact and true.

In any of the refining scenarios above, you can use invariant() as opposed to the conditional check.