【PHP8.1】PHPで列挙型ENUMが使えるようになる

PHPの開発者のひとり、[Larry Garfield](https://twitter.com/crell)がMLに[列挙型のRFCを投稿](https://externals.io/message/112417)しました。

最近[Ilija Tovilo](https://twitter.com/ilijatovilo)とふたりでPHPに代数的データ型のサポートを追加する作業を行ってるよ。
このプロジェクトは何段階かあるんだけど、まずは第一段階として列挙型を公開レビューするよ。

MLと[プルリク](https://github.com/php/php-src/pull/6489)で幾つかの突っ込みと修正が入っています。
しかし全体としては多くが賛成となっており、致命的な問題でも見つからないかぎり導入される可能性は高いでしょう。

ということで以下は[PHP RFC: Enumerations](https://wiki.php.net/rfc/enumerations)の日本語訳です。


# PHP RFC: Enumerations

## Introduction

このRFCは、PHPに列挙型を導入するものです。
このRFCの範囲は列挙型のみに限定されています。
すなわちプリミティブ型の糖衣構文などはなく、列挙型に関連する事項以外の追加情報は含まないようにしています。

列挙型の機能により、データモデリング、カスタム型の定義、モナドスタイルのサポートなどが大幅に拡充されるでしょう。
列挙型は、「無効な状態は存在できないようにする」というモデリングを可能とし、不要なテスト工数を減らし、より堅牢なコードを記述できるようになります。

多くの言語は、複数種類の列挙型をサポートしています。
[複数言語を調査した結果](https://github.com/Crell/enum-comparison)によると、一般的に3つのグループに分類できることがわかりました。
すなわちファンシーな定数、ファンシーなオブジェクト、そして完全な代数的データ型です。

このRFCは、完全な代数的データ型を導入するという大きな取り組みの一部です。
従ってこのRFCでは、今後のRFCで完全な代数的データ型に拡張できるように、"ファンシーなオブジェクト"タイプのENUMを実装します。
これはSwift、Rust、Kotlinなどの考え方の影響を受けていますが、それらを直接的にモデル化したわけではありません。

列挙型の値のもっとも一般的なケースは真偽型で、取ることのできる値は`true`と`false`のみです。
このRFCでは、開発者が独自に任意の列挙型を定義することが可能にしています。


## Proposal

列挙型は、クラスとオブジェクトを下敷きに構築されています。
すなわち、特に断りのない限り「この場合の列挙型の動作はどうなるの？」は、「オブジェクトのインスタンスと同じ」となります。
たとえばオブジェクト型チェックに成功し、大文字小文字を区別しません(オートロード時の区別がファイルシステム・クラスローダ依存なのも同じ)。

### Unit enumerations

このRFCでは、`enum`という新しい言語機能が導入されます。
Enumはクラスに似ており、クラス・インターフェイス・トレイトと同じ名前空間に存在します。
また同じようにオートロードも可能です。
列挙型は、限られた数の有効な値を持ちます。

```php
enum Suit {
  case Hearts;
  case Diamonds;
  case Clubs;
  case Spades;
}
```

この構文では、4つの値を持つ新たな列挙型`Suit`を作成しました。
値とはすなわち`Suit::Hearts`・`Suit::Diamonds`・`Suit::Clubs`・`Suit::Spades`です。
これら4値を変数などに代入することができます。
型宣言に列挙型を指定した場合は、その型の値のみを渡すことができます。

```php
$val = Suit::Diamonds;

function pick_a_card(Suit $suit) { ... }

pick_a_card($val);        // OK
pick_a_card(Suit::Clubs); // OK
pick_a_card('Spades');    // TypeError
```

列挙型は0個以上の`case`を持つことができ、上限はありません。
`case`が0個の列挙型は構文的には有効ですが、実質的にあまり意味がありません。

`case`は具体的なスカラー値に紐付けられているわけではありません。
それぞれの`case`は紐付けられているシングルトンと等しくなります。

```php
$a = Suit::Spades;
$b = Suit::Spades;

$a === $b; // true

$a instanceof Suit;         // true
$a instanceof Suit::Spades; // true
```

このタイプの列挙型(`case`のみでできていて、関連データなどは付けられない)はユニット列挙型として知られています。

これはEnumインターフェイスを実装するオブジェクトとして実装されており、また内部インターフェイスUnitEnumも実装しています。
他のオブジェクトと列挙型を区別する方法のひとつです。

```php
Suit::Hearts instanceof Enum; // true
Suit::Hearts instanceof UnitEnum; // true
```

### Enumerated Case Methods

列挙型もクラスなので、メソッドを定義することができます。

またインターフェイスをimplementsすることもできますが、その場合は全ての`case`に対して実装しなければなりません。
個々の`case`にインターフェイスをimplementsすることはできません。

```php
interface Colorful {
  public function color(): string;
}

enum Suit implements Colorful {
  case Hearts {
    public function color(): string {
      return "Red";
    }
  }
 
  case Diamonds {
    public function color(): string {
      return "Red";
    }
  }

  case Clubs {
    public function color(): string {
      return "Black";
    }
  }

  case Spades {
    public function color(): string {
      return "Black";
    }
  }

  public function shape(): string {
    return "Rectangle";
  }
}

function paint(Colorful $c) { ... }

paint(Suit::Clubs);  // Works
```

この例では、4つの`case`は全て`Suite`から継承したメソッド`color`を持っています。
`case`内のメソッドは他のメソッドと同じように動作します。

また`case`内では`$this`が定義されていて、`Case`インスタンスを参照することができます。

上記の場合、`Red`と`Black`の値を持つ`SuitColor`列挙型を定義して、それを返す方がよりよいモデリングになるでしょう。
ただそうすると例が複雑になってしまうため避けました。

上記の列挙型は、下記のように書き替えることもできます。

```php
interface Colorful {
  public function color(): string;
}

abstract class Suit implements UnitEnum, Colorful {
  public function shape(): string {
    return "Rectangle";
  }

  public function cases(): array {
    // See below.
  }
}

class Hearts extends Suit {
  public function color(): string {
    return "Red";
  }
}

class Diamonds extends Suit {
  public function color(): string {
    return "Red";
  }
}

class Clubs extends Suit {
  public function color(): string {
    return "Black";
  }
}

class Spades extends Suit {
  public function color(): string {
    return "Black";
  }
}
```

列挙型に静的メソッドを持たせ、各`case`は静的メソッドを持たないことができます。

列挙型に静的メソッドを持たせる理由は、主に代替コンストラクタのためです。
たとえば以下のような記述が可能です。

```php
enum Size {
  case Small;
  case Medium;
  case Large;

  public static function fromLength(int $cm) {
    return match(true) {
      $cm < 50 => static::Small,
      $cm < 100 => static::Medium,
      default => static::Large,
    };
  }
}
```

### Comparison to objects

列挙型は内部的にはクラスを使って実装されていて、セマンティクスの多くを共有しています。
ただし幾つかのオブジェクトスタイルの機能は禁止されています。
これらの機能は列挙型においては意味がないか、不明瞭であるか、あるいは議論の余地がある(今後追加される可能性がある)ものです。

- コンストラクタ・デストラクタ：状態を持たせなければ不要です。
- 継承：列挙型は設計上クローズドであり、継承は列挙型を壊します。
- 定数：メソッドを使います。
- プロパティ：状態を持たせません。
- 動的プロパティ：良い設計ではありません。
- マジックメソッド：別途記載する一部を除き使用できません。
- シリアライズ：シリアライズはできません。

これらの機能が必要であれば、既存のクラスの方が優れた選択肢になります。

以下のオブジェクトの機能は利用可能です。

- メソッドのpublic / protected / privateアクセス修飾子
- マジックメソッド`__get`・`__call`・`__invoke`
- 定数`__CLASS__`・`__FUNCTIONS__`

列挙型へのマジック定数`::class`はオブジェクトと全く同じ、名前空間を含む型名で評価されます。

各`case`へのマジック定数`::class`は、`::`のついた値で評価されます。
たとえば`Foo\Bar\Baz\Suit::Spades`です。

静的メソッドは`case`には使えず、列挙型自体には使用可能です。

また、各`case`は`new`で直接インスタンス化することはできず、`newInstanceWithoutConstructor`などのリフレクションでインスタンス化することもできません。


### Scalar Enums

デフォルトでは、列挙された`case`に等しいスカラ値は存在しません。
単なるシングルトンオブジェクトです。
しかし列挙型の値を他のデータベースなどにストアする可能性は高いので、シリアライズ可能なスカラ値が存在すると便利です。

以下の構文で、列挙型にスカラ値を定義することができます。

```php
enum Suit: string {
  case Hearts = 'H';
  case Diamonds = 'D';
  case Clubs = 'C';
  case Spades = 'S';
}
```

スカラ値の型はintもしくはstringであり、ひとつの列挙型は単一の型のみをサポートします。
つまり`int|string`のユニオン型はサポートされません。
列挙型がスカラ値を持つ場合は、全ての`case`は一意でなければならず、同じ値を持つことはできません。
自動インクリメントなどの自動生成スカラ値は存在しません。

スカラ列挙型の値をスカラコンテキストで使用する場合、自動的にスカラ値にダウンキャストされます。
たとえば`print`の引数に渡すとスカラ値として評価されます。

```php
print Suit::Clubs; // "C"

print "I hope I draw a " . Suit::Spades; // "I hope I draw a S".
```

スカラ列挙型は内部インターフェイス`UnitEnum`にくわえて`ScalarEnum`インターフェイスも実装されており、`ScalarEnum`インターフェイスには`from()`メソッドが定義されています。
このメソッドはスカラ値から対応する`case`を返します。
一致する`case`が存在しない場合はValueErrorをthrowします。

```php
$record = get_stuff_from_database($id);
print $record['suit'];// "H"

$suit = Suit::from($record['suit']);
$suit === Suit::Hearts; // true
```

スカラ列挙型も、ユニット列挙型同様メソッドを持つことができます。

```php
enum Suit: string {
  case Hearts = 'H';
  case Diamonds = 'D';
  case Clubs = 'C';
  case Spades = 'S' {
    public function color(): string { return 'Black'; }
  }
 
  public function color(): string
  {
    // ...
  }
}
```

### Value listing

`UnitEnum`インターフェイスには静的メソッド`cases()`が実装されています。
このメソッドは、定義された全ての`case`を順に返します。

```php
Suit::cases();
// [Suit::Hearts, Suit::Diamonds, Suit::Clubs, Suit:Spades]
```

スカラ列挙型でない場合は0から順にインデックスがつけられます。
スカラ列挙型の場合は、対応するスカラ値がキーになります。

また`ScalarEnum`インターフェイスには、スカラ値を返す`value()`メソッドが実装されています。

```php
'D' == Suit::Diamonds->value(); // true
```

### Attributes

列挙型と各caseには、他の言語要素と同じくアトリビュートを適用可能です。
Attributeクラスにふたつのターゲット定数が追加されます。
`TARGET_ENUM`は列挙型自体を対象とし、`TARGET_CASE`は`case`を対象とします。

言語が定義するアトリビュートはありません。
ユーザ定義のアトリビュートでは何でもできます。

### Match expressions

列挙値に応じてロジックを分岐させる方法として、`match`式が便利な文法を提供します。

```php
$val = Suit::Diamonds;
 
$str = match ($val) {
    Suit::Spades => "The swords of a soldier",
    Suit::Clubs => "Weapons of war",
    Suit::Diamonds => "Money for this art",
    default => "The shape of my heart",
}
```

`match`式の自然な使い方であり、`match`の構文に手を加える必要はありません。

### Reflection

列挙型のリフレクションは、`ReflectionClass`をextendsした`ReflectionEnum`クラスを使用します。
無関係なメソッド、`getProperty()`などは常に空の値を返します。
また、以下のメソッドが追加されます。

- `hasCase(string $name): bool` その`case`を持っていればtrueを返す。`$r->hasCase('Hearts')`はtrue。
- `getCases(): array` ReflectionCaseの配列を返す。
- `getCase(string $name): ReflectionCase` その名前のReflectionCaseを返す。
- `hasType(): bool` スカラ列挙型であればtrueを返す。
- `getType(): ReflectionType` スカラ列挙型であればその型を、そうでなければ空のReflectionTypeを返す。

`ReflectionCase`は、列挙型の中の個々の`case`を表します。
これも`ReflectionClass`をextendsしており、以下のメソッドが追加されます。

- `getEnum(): ReflectionEnum` caseの入っている列挙型のReflectionEnumを返す。
- `getScalar(): ?int|string` スカラ列挙型であればその値を、そうでなければnullを返す。
- `getInstance(): Enum` 該当の列挙型インスタンスを返す。


### Examples

以下に、列挙型の例をいくつか表示します。

#### Basic limited values


```php
enum SortOrder {
  case ASC;
  case DESC;
}

function query($fields, $filter, SortOrder $order) { ... }
```

`query`関数は、引数`$order`が`SortOrder::ASC`もしくは`SortOrder::DESC`のいずれかであることを保証できるようになりました。
それ以外の値を指定するとTypeErrorが発生するため、これ以上のチェックやテストは必要ありません。

#### Advanced Exclusive values

```php
enum UserStatus: string {
  case Pending = 'pending' {
    public function label(): string { 
      return 'Pending';
    }
  }
  case Active = 'active' {
    public function label(): string { 
      return 'Active';
    }
  }
  case Suspended = 'suspended' {
    public function label(): string { 
      return 'Suspended';
    }
  }
  case CanceledByUser = 'canceled' {
    public function label(): string { 
      return 'Canceled by user';
    }
  }
}
```

ユーザのステータスは`UserStatus::Pending`・`UserStatus::Active`・`UserStatus::Suspended`・`UserStatus::CanceledByUser`のいずれかであり、他の値を取ることはできません。

これら4値はポリモーフィックな`label()`メソッドを持っており、これは人間に読める文字列を返します。
この値はデータベースやHTMLのselectボックスに入れる値とは独立して指定することができます。

```php
foreach (UserStatus::cases() as $key => $val) {
  printf('<option value="%s">%s</option>\n', $key, $val->label());
}
```

`label()`メソッドは個別に実装せず、ひとつのメソッドとして列挙型クラスに実装することもできます。

```php
enum UserStatus: string {
  case Pending = 'pending';
  case Active = 'active';
  case Suspended = 'suspended';
  case CanceledByUser = 'canceled';
 
  public function label(): string {
    return match($this) {
      UserStatus::Pending => 'Pending',
      UserStatus::Active => 'Active',
      UserStatus::Suspended => 'Suspended',
      UserStatus::CanceledByUser => 'Canceled by user',
    };
  }
}
```

どちらのアプローチが適切であるかは、何をするかの目的によって異なるものであり、開発者の裁量に委ねられます。

#### State machine

列挙型は、有限ステートマシンを簡単に表現できます。

```php
enum OvenStatus {
 
  case Off {
    public function turnOn() { return OvenStatus::On; }
  }
 
  case On {
    public function turnOff() { return OvenStatus::Off; }
    public function idle() { return OvenStatus::Idle; }
  }
 
  case Idle {
    public function on() { return OvenStatus::On; }
  }
}
```

この例では、オーブンはオン、オフ、アイドルの3状態を持っています。
しかしオフからアイドル、アイドルからオフに移行することはできず、オフにするには必ずオンの状態を経由しなければなりません。
すなわち、オフから直接アイドルに移行するテストやコードを書いたりする必要がないということです。

もちろん、実際のケースではもっと追加の実装が必要になることも多いでしょうが。

### New interfaces

これまで出てきたように、このRFCでは3つの内部インターフェイスが追加されます。
このインターフェイスは、ユーザの作成したコードが列挙型であるか、列挙型である場合はどのような種類であるかを判断するために利用可能です。
ユーザが直接implementsすることはできません。

```php
interface Enum {}
 
interface UnitEnum extends Enum {
  public function cases(): array;
}
 
interface ScalarEnum extends Enum {
  public function value(): int|string;
 
  public static function from(int|string $scalar): static;
}
```

## Backward Incompatible Changes

`enum`がキーワードになります。

グローバルスコープにインターフェイス`Enum`・`UnitEnum`・`ScalarEnum`が追加されます。


## Open questions

特定の`case`に対してタイプヒントは可能？たとえば

```php
public function stuff(Suit::Heart|Suit:Diamond $card) { ... }
```

## Future Scope

[代数的データ型のRFC](https://wiki.php.net/rfc/adts)を参照ください。

### Grouped syntax

単純なユニット列挙型であれば、まとめて定義を可能にする。

```php
enum Suit {
  case Hearts, Diamonds, Clubs, Spades;
}
```

メソッドの定義されていない単純なユニット列挙型にのみ可能な文法です。
この列挙型がどのくらい使われるのか不明であり、構文も議論の余地があり、必要に応じて後から追加も可能な構文なので、現時点では対応していません。

### Enums as array keys

列挙型の`case`はオブジェクトであるため、連想配列のキーとしては使用できません。
将来的には対応するかもしれません。

### Serialization

`Suit::Clubs === unserialize(serialize(Suit::Clubs))`を安全にシリアライズする方法があれば、後から追加される可能性があります。
今のところは非対応です。


# 感想

最終的に[代数的データ型](https://wiki.php.net/rfc/adts)の実装を目指す遠大な計画の一端です。

個人的には`const`でどうにもならない場面に遭遇したことがないので、PHPにおいて列挙型がどこまで有用なのかよくわかりません。
しかし、[PHPで列挙型](https://qiita.com/Hiraku/items/71e385b56dcaa37629fe)[を作ろう](https://qiita.com/FattyRabbit/items/f16981e88380282d7390)[という](https://qiita.com/suin/items/17ee61d7e75b422a7ec3)[試みが昔から](https://qiita.com/Yorinton/items/f6138f2bca7664162ca3)[山ほど](https://qiita.com/sayama0402/items/4e8a885fed367090de12)[行われている](https://qiita.com/t_ishida/items/2e4d2aa31d69abfd48a5)ことを鑑みると、けっこうな需要はありそうです。
特に型以上の引数値限定はこれまで基本的にはできませんでしたから、この点についてはかなり便利そうです。

このような追加機能については、往々にしてユーザによる勝手拡張の方が便利だったりすることが多いですが、列挙型の場合は`extends enum`を文法で禁止することで値を完全に保証したり、値にメソッドを生やすことができたりと、公式実装の方が便利っぽいですね。
しかし、いちいち`case`を入れないといけないのは面倒ですね。
他言語では`enum Suit{Hearts, Diamonds, Clubs, Spades}`みたいに書けるものも多いのですが、PHPでは構文解析の都合上難しかったのかな？

ところで[SplEnum](https://www.php.net/manual/ja/class.splenum.php)ってどうなったんだろう。
RFCでもMLでもプルリクでも誰一人話題に上げていない。