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.

Optional Fields

All fields are required, unless explicitly marked as optional. Since HHVM 3.23, nullable fields are no longer considered optional.

Fields are marked optional by preceding the field name with a ? token:

<?hh // strict

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

// 'z' field is optional
type Point = shape('x' => int, 'y' => int, ?'z' => int);

function get_2d_point(): Point {
  return shape('x' => 123, 'y' => 456);
}

function get_3d_point(): Point {
  return shape('x' => 123, 'y' => 456, 'z' => 789);
}

Nullable fields are not optional - using them interchangeably will cause typechecker errors:

<?hh // strict

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

type PointWithOptionalZ = shape('x' => int, 'y' => int, ?'z' => int);
type PointWithNullableZ = shape('x' => int, 'y' => int, 'z' => ?int);

function optional_is_not_nullable(): PointWithOptionalZ {
  // Invalid return type (Typing[4110]
  //   This is an int
  //   It is incompatible with a nullable type 
  return shape('x' => 123, 'y' => 456, 'z' => null);
}

function nullable_is_not_optional(): PointWithNullableZ {
  // Invalid return type (Typing[4057])
  //  The field 'z' is missing
  //  The field 'z' is defined
  return shape('x' => 123, 'y' => 456);
}

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.