Built In Types: Enum Class
In comparison to enumerated types (enums), enum classes are not restricted to int and string values.
Enum types v. Enum class
Built-in enum types limit the base type of an enum to arraykey
-- an integer or string -- or another enum.
The base type of an enum class can be any type: they are not required to be constant expressions and objects are valid values. However, with Generics, if an enum has type T
as a base type, its enum values are bound to values whose type are a subtype of T
.
Declaring a new enum class
Enum classes are syntactically different from enum types, as they require:
- the
enum class
keyword rather than theenum
keyword - that each value is annotated with its precise type: for example,
string
instring s = ...
Example: Simple declarations
// Enum class where we allow any type
enum class Random: mixed {
int X = 42;
string S = 'foo';
}
// Enum class that mimics a normal enum (only allowing ints)
enum class Ints: int {
int A = 0;
int B = 10;
}
Example: Interface as a Base Type
// Some class definitions to make a more involved example
interface IHasName {
public function name(): string;
}
class HasName implements IHasName {
public function __construct(private string $name)[] {}
public function name(): string {
return $this->name;
}
}
class ConstName implements IHasName {
public function name(): string {
return "bar";
}
}
// enum class which base type is the IHasName interface: each enum value
// can be any subtype of IHasName, here we see HasName and ConstName
enum class Names: IHasName {
HasName Hello = new HasName('hello');
HasName World = new HasName('world');
ConstName Bar = new ConstName();
}
Accessing values
Once declared, enum values are accessed using the ::
operator: Names::Hello
, Names::Bar
, ...
Control over enum values
Using coeffects, you can have control over which expressions are allowed as enum class constants.
By default, all enum classes are under the write_props
context. It is not possible to override this explicitly.
Extending an Existing enum class (Inheritance)
Enum classes can be composed together, as long as they implement the same base type:
interface IBox {}
class Box<T> implements IBox {
public function __construct(public T $data)[] {}
}
enum class Boxes: IBox {
Box<int> Age = new Box(42);
Box<string> Color = new Box('red');
Box<int> Year = new Box(2021);
}
function get<T>(\HH\MemberOf<Boxes, Box<T>> $box): T {
return $box->data;
}
function test0(): void {
get(Boxes::Age); // ok, of type int, returns 42
get(Boxes::Color); // ok, of type string, returns 'red'
get(Boxes::Year); // ok, of type int, returns 2021
}
enum class EBase: IBox {
Box<int> Age = new Box(42);
}
enum class EExtend: IBox extends EBase {
Box<string> Color = new Box('red');
}
In this example, EExtend
inherits Age
from EBase
, which means that EExtend::Age
is defined.
As with ordinary class extension, using the extends
keyword will create a subtype relation between the enums: EExtend <: EBase
.
Enum classes support multiple inheritance as long as there is no ambiguity in value names, and that each enum class uses the same base type:
enum class E: IBox {
Box<int> Age = new Box(42);
}
enum class F: IBox {
Box<string> Name = new Box('foo');
}
enum class X: IBox extends E, F { } // ok, no ambiguity
enum class E0: IBox extends E {
Box<int> Color = new Box(0);
}
enum class E1: IBox extends E {
Box<string> Color = new Box('red');
}
// enum class Y: IBox extends E0, E1 { }
// type error, Y::Color is declared twice, in E0 and in E1
// only he name is use for ambiguity
Diamond shape scenarios
Enum classes support diamond shaped inheritance as long as there is no ambiguity, like in:
enum class DiamondBase: IBox {
Box<int> Age = new Box(42);
}
enum class D1: IBox extends DiamondBase {
Box<string> Name1 = new Box('foo');
}
enum class D2: IBox extends DiamondBase {
Box<string> Name2 = new Box('bar');
}
enum class D3: IBox extends D1, D2 {}
<<__EntryPoint>>
function main(): void {
echo D3::Age->data;
}
Here there is no ambiguity: the constant Age
is inherited from DiamondBase
, and only from there.
The main
function will echo 42
as expected.
If either D1
, D2
or D3
tries to define a constant named Age
, there will be an error.
Control over inheritance
Though the final
keyword is not supported, Enum classes support the __Sealed
attribute. Using __Sealed
, you can specify which other enum classes, if any, are allowed to extend from your enum class.
Abstract enum classes
Like regular classes, enum classes come in two flavors: concrete and abstract. An abstract enum class can declare abstract members (constants), where only their type and name are provided.
// abstract enum class with some abstract members
abstract enum class AbstractNames: IHasName {
abstract HasName Foo;
HasName Bar = new HasName('bar');
}
Abstract members do not support default values, and can't be accessed directly. They only map a name to a type. You must extend your abstract enum class into a concrete one with implementations of all abstract members to safely access members defined as abstract.
enum class ConcreteNames: IHasName extends AbstractNames {
HasName Foo = new HasName('foo'); // one must provide all the abstract members
// Bar is inherited from AbstractNames
}
All concrete members are inherited, and can't be overriden.
Defining a Function that expects an enum class
When defining a function that expects an enum class value (e.g. Foo::BAR
), you need to define the expected parameter appropriately with HH\MemberOf
or you will run into errors.
enum class Foo: string {
string BAR = 'BAZ';
}
function do_stuff(Foo $value): void {
var_dump($value);
}
function main(): void {
do_stuff(Foo::BAR); // expected Foo but got string
}
However, if we instead define do_stuff()
as receiving HH\MemberOf<Foo, string>
, then we can use Foo::Bar
with no issues.
enum class Foo: string {
string BAR = 'BAZ';
}
function do_stuff(HH\MemberOf<Foo, string> $value): void {
var_dump($value);
}
function main(): void {
do_stuff(Foo::BAR); // ok
}
Accessing enum class types
An enum class type is more informative than a traditional built-in enum type.
Let's examine enum E
v. enum class EC
.
enum E: int {
A = 42;
}
enum class EC: int {
int A = 42;
}
The built-in enum type of E::A
is just E
. All we know is that value A
is declared within the enum: we know nothing of its underlying type.
But if we look at the enum class EC::A
its type is HH\MemberOf<EC, int>
. We know that it's declared within the enum class EC
, with type int
.
Declaring type constants in an enum class
Like normal classes, enum classes can declare type constants. Abstract type constants are also supported:
interface IGet<+T> {
public function get(): T;
}
class Box<T> implements IGet<T> {
public function __construct(private T $data)[] {}
public function get(): T { return $this->data; }
public function set(T $data): void { $this->data = $data; }
}
abstract enum class E : IGet<mixed> {
abstract const type T;
abstract Box<this::T> A;
Box<int> B = new Box(42);
}
enum class F : IGet<mixed> extends E {
const type T = string;
Box<this::T> A = new Box('zuck');
}
Full Example: Dependent Dictionary
First, a couple of general Hack definitions:
function expect_string(string $str): void {
echo 'expect_string called with: '.$str."\n";
}
interface IKey {
public function name(): string;
}
abstract class Key<T> implements IKey {
public function __construct(private string $name)[] {}
public function name(): string {
return $this->name;
}
public abstract function coerceTo(mixed $data): T;
}
class IntKey extends Key<int> {
public function coerceTo(mixed $data): int {
return $data as int;
}
}
class StringKey extends Key<string> {
public function coerceTo(mixed $data): string {
// random logic can be implemented here
$s = $data as string;
// let's make everything in caps
return Str\capitalize($s);
}
}
Now let’s create the base definitions for our dictionary
enum class EKeys: IKey {
// here are a default key, but this could be left empty
Key<string> NAME = new StringKey('NAME');
}
abstract class DictBase {
// type of the keys, left abstract for now
abstract const type TKeys as EKeys;
// actual data storage
private dict<string, mixed> $raw_data = dict[];
// generic code written once which enforces type safety
public function get<T>(\HH\MemberOf<this::TKeys, Key<T>> $key): ?T {
$name = $key->name();
$raw_data = idx($this->raw_data, $name);
// key might not be set
if ($raw_data is nonnull) {
$data = $key->coerceTo($raw_data);
return $data;
}
return null;
}
public function set<T>(\HH\MemberOf<this::TKeys, Key<T>> $key, T $data): void {
$name = $key->name();
$this->raw_data[$name] = $data;
}
}
Now one just need to provide a set of keys and extends DictBase
:
class Foo { /* user code in here */ }
class MyKeyType extends Key<Foo> {
public function coerceTo(mixed $data): Foo {
// user code validation
return $data as Foo;
}
}
enum class MyKeys: IKey extends EKeys {
Key<int> AGE = new IntKey('AGE');
MyKeyType BLI = new MyKeyType('BLI');
}
class MyDict extends DictBase {
const type TKeys = MyKeys;
}
<<__EntryPoint>>
function main(): void {
$d = new MyDict();
$d->set(MyKeys::NAME, 'tony');
$d->set(MyKeys::BLI, new Foo());
// $d->set(MyKeys::AGE, new Foo()); // type error
expect_string($d->get(MyKeys::NAME) as nonnull);
}