Classes: Inheritance

Hack supports inheritance, a means by which a derived class can extend and specialize a single base class. However, unlike some other languages, classes in Hack are not all derived from a common ancestor.

Every user-defined exception class must extend the library class Exception. For example:

class MyRangeException extends \Exception {
  public function __construct(string $message, int $val) {
    parent::__construct($message);
    // ...
  }
  // ...
}

<<__EntryPoint>>
function main(): void {
  try {
    throw new MyRangeException("xyz", 123);
  } catch (MyRangeException $e) {
    // ...
  }
}

An abstract class is a base type intended for derivation, but which cannot be instantiated directly. A concrete class is a class that is not abstract. In the following example, Vehicle and Aircraft are abstract types while PassengerJet is concrete:

abstract class Vehicle {
  public abstract function get_max_speed(): int;
  // ...
}

abstract class Aircraft extends Vehicle {
  public abstract function get_max_altitude(): int;
  // ...
}

class PassengerJet extends Aircraft {
  public function get_max_speed(): int {
    // implement method
    return 500;
  }
  public function get_max_altitude(): int {
    // implement method
    return 35000;
  }
  // ...
}

<<__EntryPoint>>
function main(): void {
  $pj = new PassengerJet();
  echo "\$pj's maximum speed: ".$pj->get_max_speed()."\n";
  echo "\$pj's maximum altitude: ".$pj->get_max_altitude()."\n";
}
Output
$pj's maximum speed: 500
$pj's maximum altitude: 35000

As every Vehicle has a maximum speed, class Vehicle declares a method to return that value. However, base class Vehicle does not know at runtime if it's the base of a racing car, a sailplane, or a bicycle, so it can't actually calculate that value. However, by being abstract that forces any concrete classes ultimately derived from that base to implement that method. Similarly, base class Aircraft forces its concrete descendants to implement method get_max_altitude.

A final class is one from which other classes cannot be derived. For example:

final class MathLibrary {
  private function __construct() {}   // disallows instantiation
  public static function sin(float $p): float { ... }
  ...
}
$v = MathLibrary::sin(2.34);

Having the final modifier precludes MathLibrary from being extended. Separately, by making the constructor private, we cannot create objects of this type; it's just a wrapper for a set of static members.

The members of a base class can be overridden in a derived class by redeclaring them with the same signature defined in the base class. However, an overriding constructor need not have the same signature as defined in the base class (see example below). (An overriding constructor in a derived class must have the same or a less-restricted visibility than that being overridden in the base class.)

If classes in a derived-class hierarchy have constructors, it is the responsibility of the constructor at each level to call the constructor in its base-class explicitly, using the notation parent::__construct(...). If a constructor calls its base-class constructor, it should do so as its first statement, so the object hierarchy is built from the bottom-up. A constructor should not call its base-class constructor more than once. A call to a base-class constructor searches for the nearest constructor in the class hierarchy. Not every level of the hierarchy need have a constructor. For example:

enum Color: int {
  // ...
}

class Point {
  public function __construct(num $x, num $y) {
    // ...
  }
}

class ColoredPoint extends Point {
  public function __construct(num $x, num $y, Color $col) {
    parent::__construct($x, $y); // initialize the base Point
    // ...
  }
}

A base class can define an abstract type constant—essentially a name without a concrete type attached—and subclasses each override that with a concrete type constant. Conceptually, type constants are to types, as abstract methods are to methods. A type constant has public visibility and is implicitly static. By convention, a type constant's name begins with an uppercase T. Consider the following:

abstract class CBase {
  abstract const type T;
  public function __construct(protected this::T $value) {}
}

class Cstring extends CBase {
  const type T = string;
  public function get_string(): string {
    return $this->value; // gets the string
  }
}

class Cint extends CBase {
  const type T = int;
  public function get_int(): int {
    return $this->value; // gets the int
  }
}

<<__EntryPoint>>
function run2(): void {
  \var_dump((new Cstring('abc'))->get_string());
  \var_dump((new Cint(123))->get_int());
}