PHPの開発者のひとり、Larry GarfieldがMLに列挙型のRFCを投稿しました。
最近Ilija ToviloとふたりでPHPに代数的データ型のサポートを追加する作業を行ってるよ。
このプロジェクトは何段階かあるんだけど、まずは第一段階として列挙型を公開レビューするよ。
MLとプルリクで幾つかの突っ込みと修正が入っています。
しかし全体としては多くが賛成となっており、致命的な問題でも見つからないかぎり導入される可能性は高いでしょう。
ということで以下はPHP RFC: Enumerationsの日本語訳です。
PHP RFC: Enumerations
Introduction
このRFCは、PHPに列挙型を導入するものです。
このRFCの範囲は列挙型のみに限定されています。
すなわちプリミティブ型の糖衣構文などはなく、列挙型に関連する事項以外の追加情報は含まないようにしています。
列挙型の機能により、データモデリング、カスタム型の定義、モナドスタイルのサポートなどが大幅に拡充されるでしょう。
列挙型は、「無効な状態は存在できないようにする」というモデリングを可能とし、不要なテスト工数を減らし、より堅牢なコードを記述できるようになります。
多くの言語は、複数種類の列挙型をサポートしています。
複数言語を調査した結果によると、一般的に3つのグループに分類できることがわかりました。
すなわちファンシーな定数、ファンシーなオブジェクト、そして完全な代数的データ型です。
このRFCは、完全な代数的データ型を導入するという大きな取り組みの一部です。
従ってこのRFCでは、今後のRFCで完全な代数的データ型に拡張できるように、"ファンシーなオブジェクト"タイプのENUMを実装します。
これはSwift、Rust、Kotlinなどの考え方の影響を受けていますが、それらを直接的にモデル化したわけではありません。
列挙型の値のもっとも一般的なケースは真偽型で、取ることのできる値はtrue
とfalse
のみです。
このRFCでは、開発者が独自に任意の列挙型を定義することが可能にしています。
Proposal
列挙型は、クラスとオブジェクトを下敷きに構築されています。
すなわち、特に断りのない限り「この場合の列挙型の動作はどうなるの?」は、「オブジェクトのインスタンスと同じ」となります。
たとえばオブジェクト型チェックに成功し、大文字小文字を区別しません(オートロード時の区別がファイルシステム・クラスローダ依存なのも同じ)。
Unit enumerations
このRFCでは、enum
という新しい言語機能が導入されます。
Enumはクラスに似ており、クラス・インターフェイス・トレイトと同じ名前空間に存在します。
また同じようにオートロードも可能です。
列挙型は、限られた数の有効な値を持ちます。
enum Suit {
case Hearts;
case Diamonds;
case Clubs;
case Spades;
}
この構文では、4つの値を持つ新たな列挙型Suit
を作成しました。
値とはすなわちSuit::Hearts
・Suit::Diamonds
・Suit::Clubs
・Suit::Spades
です。
これら4値を変数などに代入することができます。
型宣言に列挙型を指定した場合は、その型の値のみを渡すことができます。
$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
は紐付けられているシングルトンと等しくなります。
$a = Suit::Spades;
$b = Suit::Spades;
$a === $b; // true
$a instanceof Suit; // true
$a instanceof Suit::Spades; // true
このタイプの列挙型(case
のみでできていて、関連データなどは付けられない)はユニット列挙型として知られています。
これはEnumインターフェイスを実装するオブジェクトとして実装されており、また内部インターフェイスUnitEnumも実装しています。
他のオブジェクトと列挙型を区別する方法のひとつです。
Suit::Hearts instanceof Enum; // true
Suit::Hearts instanceof UnitEnum; // true
Enumerated Case Methods
列挙型もクラスなので、メソッドを定義することができます。
またインターフェイスをimplementsすることもできますが、その場合は全てのcase
に対して実装しなければなりません。
個々のcase
にインターフェイスをimplementsすることはできません。
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
列挙型を定義して、それを返す方がよりよいモデリングになるでしょう。
ただそうすると例が複雑になってしまうため避けました。
上記の列挙型は、下記のように書き替えることもできます。
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
は静的メソッドを持たないことができます。
列挙型に静的メソッドを持たせる理由は、主に代替コンストラクタのためです。
たとえば以下のような記述が可能です。
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
に等しいスカラ値は存在しません。
単なるシングルトンオブジェクトです。
しかし列挙型の値を他のデータベースなどにストアする可能性は高いので、シリアライズ可能なスカラ値が存在すると便利です。
以下の構文で、列挙型にスカラ値を定義することができます。
enum Suit: string {
case Hearts = 'H';
case Diamonds = 'D';
case Clubs = 'C';
case Spades = 'S';
}
スカラ値の型はintもしくはstringであり、ひとつの列挙型は単一の型のみをサポートします。
つまりint|string
のユニオン型はサポートされません。
列挙型がスカラ値を持つ場合は、全てのcase
は一意でなければならず、同じ値を持つことはできません。
自動インクリメントなどの自動生成スカラ値は存在しません。
スカラ列挙型の値をスカラコンテキストで使用する場合、自動的にスカラ値にダウンキャストされます。
たとえばprint
の引数に渡すとスカラ値として評価されます。
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します。
$record = get_stuff_from_database($id);
print $record['suit'];// "H"
$suit = Suit::from($record['suit']);
$suit === Suit::Hearts; // true
スカラ列挙型も、ユニット列挙型同様メソッドを持つことができます。
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
を順に返します。
Suit::cases();
// [Suit::Hearts, Suit::Diamonds, Suit::Clubs, Suit:Spades]
スカラ列挙型でない場合は0から順にインデックスがつけられます。
スカラ列挙型の場合は、対応するスカラ値がキーになります。
またScalarEnum
インターフェイスには、スカラ値を返すvalue()
メソッドが実装されています。
'D' == Suit::Diamonds->value(); // true
Attributes
列挙型と各caseには、他の言語要素と同じくアトリビュートを適用可能です。
Attributeクラスにふたつのターゲット定数が追加されます。
TARGET_ENUM
は列挙型自体を対象とし、TARGET_CASE
はcase
を対象とします。
言語が定義するアトリビュートはありません。
ユーザ定義のアトリビュートでは何でもできます。
Match expressions
列挙値に応じてロジックを分岐させる方法として、match
式が便利な文法を提供します。
$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
enum SortOrder {
case ASC;
case DESC;
}
function query($fields, $filter, SortOrder $order) { ... }
query
関数は、引数$order
がSortOrder::ASC
もしくはSortOrder::DESC
のいずれかであることを保証できるようになりました。
それ以外の値を指定するとTypeErrorが発生するため、これ以上のチェックやテストは必要ありません。
Advanced Exclusive values
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ボックスに入れる値とは独立して指定することができます。
foreach (UserStatus::cases() as $key => $val) {
printf('<option value="%s">%s</option>\n', $key, $val->label());
}
label()
メソッドは個別に実装せず、ひとつのメソッドとして列挙型クラスに実装することもできます。
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
列挙型は、有限ステートマシンを簡単に表現できます。
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することはできません。
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
に対してタイプヒントは可能?たとえば
public function stuff(Suit::Heart|Suit:Diamond $card) { ... }
Future Scope
代数的データ型のRFCを参照ください。
Grouped syntax
単純なユニット列挙型であれば、まとめて定義を可能にする。
enum Suit {
case Hearts, Diamonds, Clubs, Spades;
}
メソッドの定義されていない単純なユニット列挙型にのみ可能な文法です。
この列挙型がどのくらい使われるのか不明であり、構文も議論の余地があり、必要に応じて後から追加も可能な構文なので、現時点では対応していません。
Enums as array keys
列挙型のcase
はオブジェクトであるため、連想配列のキーとしては使用できません。
将来的には対応するかもしれません。
Serialization
Suit::Clubs === unserialize(serialize(Suit::Clubs))
を安全にシリアライズする方法があれば、後から追加される可能性があります。
今のところは非対応です。
感想
最終的に代数的データ型の実装を目指す遠大な計画の一端です。
個人的にはconst
でどうにもならない場面に遭遇したことがないので、PHPにおいて列挙型がどこまで有用なのかよくわかりません。
しかし、PHPで列挙型を作ろうという試みが昔から山ほど行われていることを鑑みると、けっこうな需要はありそうです。
特に型以上の引数値限定はこれまで基本的にはできませんでしたから、この点についてはかなり便利そうです。
このような追加機能については、往々にしてユーザによる勝手拡張の方が便利だったりすることが多いですが、列挙型の場合はextends enum
を文法で禁止することで値を完全に保証したり、値にメソッドを生やすことができたりと、公式実装の方が便利っぽいですね。
しかし、いちいちcase
を入れないといけないのは面倒ですね。
他言語ではenum Suit{Hearts, Diamonds, Clubs, Spades}
みたいに書けるものも多いのですが、PHPでは構文解析の都合上難しかったのかな?
ところでSplEnumってどうなったんだろう。
RFCでもMLでもプルリクでも誰一人話題に上げていない。