Shapes: Introduction

A shape consists of a group of zero or more data fields taken together as a whole; it is an array whose keys are tracked by the Hack typechecker.

For example:

<?hh
shape('x' => int, 'y' => int)

The definition of a shape contains an ordered set of fields each of which has a name and a type. In the above case, the shape consists of two int fields, with the names 'x' and 'y', respectively.

<?hh

namespace Hack\UserDocumentation\Shapes\Introduction\Examples\Intro;

type Point = shape('x' => int, 'y' => int);

class C1 {
  private Point $origin;
  private function __construct(int $x = 0, int $y = 0) {
    $this->origin = shape('x' => $x, 'y' => $y);
  }
}

function distance_between_2_Points(Point $p1, Point $p2): float {
  // access shape info via keys in the shape map, in this case `x` and `y`
  $dx = $p1['x'] - $p2['x'];
  $dy = $p1['y'] - $p2['y'];
  return sqrt($dx*$dx + $dy*$dy);
}

function run(): void {
  $p1 = shape('x' => 4, 'y' => 6);
  $p2 = shape('x' => 9, 'y' => 2);
  var_dump(distance_between_2_Points($p1, $p2));
}

run();
Output
float(6.4031242374328)

Although we can use a shape type directly, oftentimes it is convenient to create an alias, such as the name Point above, and use that instead.

Accessing Fields

A field in a shape is accessed using its name as the key in a subscript-expression that operates on a shape of the corresponding shape type. For example:

The name of a field can be written in one of two possible forms:

  • A single-quoted string (as shown in the example above)
  • A class constant of type string or int

Note that an integer literal cannot be used directly as a field name.

The names of all fields in a given shape definition must be distinct and have the same form.

All Non-Nullable Fields Are Required

When returning your shape from a function, for example, all fields must be accounted for; otherwise the Hack typechecker will raise an error.

<?hh

namespace Hack\UserDocumentation\Shapes\Introduction\Examples\Fields;

type user = shape('id' => int, 'name' => string);

class UserClass {
  public static function create_user(int $id, string $name): user {
    $user = shape();
    $user['id'] = $id;
    return $user;
  }
}

function run(): void {
  var_dump(UserClass::create_user(1, 'James'));
}

run();
Output
array(1) {
  ["id"]=>
  int(1)
}

In the above example, we forgot to set the name field of the shape. Note that HHVM will still run the code since it really is just an array under the covers.

Nullable Fields Are Optional

The typechecker conflates optional and nullable. shape('name' => ?string) will match both shape('name' => null) and shape(). Accordingly, the typechecker will not raise an error if you omit a nullable field. The runtime, however, will raise an OutOfBoundsException if you try to read an omitted field.

Class Constants

Class constants can be used in shapes.

<?hh

namespace Hack\UserDocumentation\Shapes\Introduction\Examples\ClassConstants;

class C2 {
  const string KEYA = 'x';
  const string KEYB = 'y';
  const int KEYX = 10;
  const int KEYY = 23;
}

type PointS = shape(C2::KEYA => int, C2::KEYB => int);
type PointI = shape(C2::KEYX => int, C2::KEYY => int);

function print_pointS(PointS $p): void {
  var_dump($p);
}

function print_pointI(PointI $p): void {
  var_dump($p);
}

function run(): void {
  print_pointI(shape(C2::KEYX => -1, C2::KEYY => 2));
  print_pointS(shape(C2::KEYA => -1, C2::KEYB => 2));
}

run();
Output
array(2) {
  [10]=>
  int(-1)
  [23]=>
  int(2)
}
array(2) {
  ["x"]=>
  int(-1)
  ["y"]=>
  int(2)
}

In the case of the integer class constants in our example above, by arbitrary choice, the x-coordinate is stored in the element with key 10, while the y-coordinate is stored in the element with key 23.

Shapes Without Type Aliases

A shape does not have to have a type alias associated with it. Here is an example of just using the literal shape syntax in all places.

<?hh

namespace Hack\UserDocumentation\Shapes\Intro\Examples\Anonymous;

class C {

  public function __construct(
    private shape('real' => float, 'imag' => float) $prop) {}

  public function setProp(shape('real' => float, 'imag' => float) $val): void {
    $this->prop = shape('real' => $val['real'], 'imag' => $val['imag']);
  }

  public function getProp(): shape('real' => float, 'imag' => float) {
    return $this->prop;
  }
}

function main(): void {
  $c = new C(shape('real' => -2.5, 'imag' => 1.3));
  var_dump($c);
  $c->setProp(shape('real' => 2.0, 'imag' => 99.3));
  var_dump($c->getProp());
}

main();
Output
object(Hack\UserDocumentation\Shapes\Intro\Examples\Anonymous\C)#1 (1) {
  ["prop":"Hack\UserDocumentation\Shapes\Intro\Examples\Anonymous\C":private]=>
  array(2) {
    ["real"]=>
    float(-2.5)
    ["imag"]=>
    float(1.3)
  }
}
array(2) {
  ["real"]=>
  float(2)
  ["imag"]=>
  float(99.3)
}

Caveats

Shapes are arrays; i.e., a call to is_array() will return true. However there are some things you can do with arrays that you cannot do with shapes.

  • You cannot read or write with unknown keys. e.g., $shape[$var] is invalid. The key must be a string literal or class constant.
  • You cannot use the array append [] operator on a shape.
  • You cannot foreach on shape since it doesn't implement Traversable or Container.