Built In Types: Enum Class
Disclaimer: This is a new feature, and you will have to enable it in your projects
Historically, the base type of an enumerated type (enum) is restricted to the arraykey
type: it must be an integer, a string or another enum.
This feature enables more types as base type for enumerations, as long as the type doesn’t have any generics:
if an enum has type t
as base type, then its enum constants are bound to values whose type is a subtype of t
.
Our main goal here is to allow more expressivity and replace some generated / boiler plate code: associating enum constants to different subtypes of the base type provides a lightweight form of dependent types that can be used as type-safe abstraction to write generic validator/sanitiser/loggers boiler-plate code.
Here are a couple of simple examples:
// Simple enum class where we allow any type
enum class Random : mixed {
int X = 42;
string S = 'foo';
}
// enum classes that mimics a normal enum
enum class Ints : int {
int A = 0;
int B = 10;
}
// 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 constant
// 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();
}
Declaring a new enum class
Enum classes are syntactically different from existing enum types, as they require:
- the
enum class
keyword rather than theenum
keyword - that each constant is annotated with its precise type, as in
HasName Hello = ...
Once declared, enum class constants are accessed like normal enum constants using the ::
operator: Names::Hello
, Names::Bar
, ...
The HH\MemberOf
alias
Another difference is that their types are more informative. Consider the enum:
enum E : int {
A = 42;
}
The type of E::A
is just E
.
enum class EC : int {
int A = 42;
}
If we now look at EC::A
, its type is HH\MemberOf<EC, int>
.
Let's have a look back at Names::World
, its type is HH\MemberOf<Names, HasName>
, and Names::Bar
has type HH\MemberOf<Names, ConstName>
.
The addition of this type layer is here to give more information about the enumeration, namely its exact type
(HasName
vs IHasName
) and from which enum class it comes from (Names
).
HH\MemberOf
is designed to allow a transparent access to the underlying value: HH\MemberOf<Names, HasName> <: HasName
.
This means that this layer can be ignored if one doesn’t need the additional information, and Names::Hello
can be used directly as an instance of the HasName
class:
function expect_name(HasName $x) : void {}
// Names::Hello has type HH\MemberOf<Names, HasName>
function test0(): void {
expect_name(Names::Hello); // ok !
}
Defining function that expects enum class
As previously explained, it is completely fine to use enum class constants as an instance of their declared type:
function show_name_interface(IHasName $x) : string {
return $x->name();
}
function show_name(HasName $x) : string {
return $x->name();
}
function test1(): void {
show_name(new HasName('toto')); // Ok
show_name_interface(Names::Bar); // Ok
show_name(Names::Hello); // Ok
// show_name(new ConstName()); // error, ConstName is not a subtype of HasName
// show_name(Names::Bar); // error, ConstName is not a subtype of HasName
}
To access the additional information added by HH\MemberOf
, one has to change the function signature in the following way:
function show_name_from_Names(\HH\MemberOf<Names, IHasName> $x): string {
echo "Showing names from the enum class `Names` only";
return $x->name(); // HH\MemberOf is transparent to the runtime
}
function test2(): void {
show_name(new HasName('toto')); // error, this instance is not from the Names enum
show_name(Names::World); // no problem
}
enum class OtherNames: IHasName {
HasName Foo = new HasName('foo');
}
function test3(): void {
show_name(OtherNames::Foo); // error, expected Names but got OtherNames
}
As explained in the introduction, this feature also allows to write dependently typed code. Let’s consider the more involved code:
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
}
Here we have a simple example of dependent typing: the return value of the get
function depends on which constant is passed as an input. We can even make it more strict:
function get_int(\HH\MemberOf<Boxes, Box<int>> $box) : int {
return $box->data;
}
function test1(): void {
get_int(Boxes::Age); // ok
// get_int(Boxes::Color); // type error, Color is not a Box<int>
}
Extending an existing enum class
Enum classes can be composed together, as long as they implement the same base type:
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 the names of the constants, 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
Control over inheritance
Enum classes support the __Sealed
attribute, just like normal classes. This will enable a more fine grain control over the extension mechanics.
However enum classes do not yet support the final
keyword.
Control over enum class constants
Using coeffects, one can have control over what kind of expressions are allowed as enum class constants. Please refer to the coeffects documentation for more details about this feature.
By default, all enum classes are under the write_props
context. It is not possible to override this explicitly. The only work around must be a temporary one involving HH_FIXME
s.
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);
}
expect_string called with: Tony
How to enable the feature
To use this new feature, you need to pass the following flags to HHVM/Hack
- hhvm flags:
-d hhvm.hack.lang.enable_enum_classes=1
.hhconfig
flags:enable_enum_classes=true