Hhvm: Hack: Allow extending shapes

Created on 5 Jun 2016  路  9Comments  路  Source: facebook/hhvm

We're starting to move towards using shapes as option arguments and are having to duplicate most of the declaration between subclasses, which leads to problems when changing the base class.

Contrived example:

<?hh // strict

type AOptions = shape(
  'a' => int,
  'b' => ?string,
);

type BOptions = shape(
  'a' => int,
  'b' => ?string,
  'c' => (function():void),
); 

class A {
  public function __construct(protected AOptions $options) {}
}

class B extends A {
  protected (function():void) $c;

  public function __construct(BOptions $options) {
    $this->c = $options['c'];
    parent::__construct($options);
  }
}

Being able to have BOptions extend AOptions would reduce the copy pasta and chance of errors. Something like:

type BOptions = shape(
  'c' => (function():void),
) extends AOptions;
feature request hack

All 9 comments

I'm not sure what the current team would think of this (cc @dabek who did a bunch of shapes work), but I think you can do this in the meantime:

type BOptions = shape(
  'c' => (function(): void),
  'aoptions' => AOptions,
);

Has anyone given anymore thought to this? It would be great to see this. Maybe even as

type BOptions = AOptions & shape(
  'c' => (function(): void),
);

8504 Just linking two related issues together by ID.

A syntax like this could also work (similar to Go).

type BOptions = shape(
  'c' => (function(): void),
  AOptions,
);

This is a common issue for our codebase as well.

Example use case: We want to write a function that takes in some input shape and adds one or more fields to it before passing it along somewhere else. We're left deciding between duplicating the entire definition or defining the field as optional on the original shape, which is strictly worse since we'd like to be able to statically differentiate shapes that are known to have that key and ones that are known not to have it.

@Wilfred I think your work on "records" addresses this?

Yeah, there's definitely been some interest in allowing shapes to be combined in a less verbose way. I'm not aware of anyone working on it currently though.

In the original example, BOptions can't be used in the place of AOptions, because AOptions isn't allowed to have extra fields. I'd make AOptions an 'open shape' by using ... like this:

type AOptions = shape(
  'a' => int,
  'b' => ?string,
  ...
);

type BOptions = shape(
  'a' => int,
  'b' => ?string,
  'c' => (function():void),
);

This is still verbose though.

A super kludgey way to make sure that BOptions includes all of AOptions would be to write a function like this:

function verify_boptions_includes_aoptions(BOptions $b): void {
  $takes_aoptions = (AOptions $_) ==> {};
  $takes_aoptions($b);
}

If it type checks, you know that all of AOptions keys are included in BOptions with compatible types!

But records will support inheritance, or won't they? So once those ship, people can use records instead of shapes for this.

As @jjergus mentions, we're currently exploring adding records to Hack. They're basically copy-on-write structs with inheritance.

abstract record Vehicle {
  int numWheels = 4;
}

record Car extends Vehicle {
  string model;
}

Since HHVM knows the number of fields in Car will never change, it can store the fields in contiguous memory (like a C array, rather than a hash map). This should use less memory and potentially be faster than shapes or darrays.

There's still a bunch of questions we're looking at though: we don't have a good syntax for field access/update yet, and we need a good story around shape interop/migration. Shapes are ordered (shape('x' => 0, 'y' => 0) === shape('y' => 0, 'x' => 0) is false) and we want to avoid that for records.

Turns out it's possible to use newtype to enforce a relationship between shape definitions.

newtype S1 = shape('x' => int, ...);

newtype S2 as S1 = shape('x' => int, 'y' => string);

You still have the duplication, but it's way less ugly than the verify_boptions_includes_aoptions example I showed above.

Was this page helpful?
0 / 5 - 0 ratings