タイトルの意味はよくわからない。
さてPHPでは、PHP8.0で型のOR、PHP8.1で型のANDが使えるようになりました。
しかし、この両者を組み合わせて使うことはできません。
function f(A | B | C $param){} // OK
function f(A & B & C $param){} // OK
function f(A | B & C $param){} // NG ←
ということで、これを可能にしようというRFCが提出されました。
既に投票は終わっており、賛成多数で可決されました。
PHP8.2からDNF型が使用可能になります。
以下は該当のRFC、Disjunctive Normal Form Typesの日本語訳です。
PHP RFC: Disjunctive Normal Form Types
Introduction
Disjunctive Normal Form (DNF) とは、論理式を正規化する標準的な方法のひとつです。
具体的には、ANDをまとめたものをORで並べる形に構造化します。
これを型宣言に適用すると、PHPのパーサーが扱えるUNION型とIntersection型を組み合わせた書き方が可能になります。
Proposal
Examples
これ以降の例では、次の定義が存在するものとします。
interface A {}
interface B {}
interface C extends A {}
interface D {}
class W implements A {}
class X implements B {}
class Y implements A, B {}
class Z extends Y implements C {}
本RFCでは、以下のような形で、プロパティ・引数・返り値にDNFで型宣言を行うことができるようになります。
// A型かつB型、もしくはD型
(A&B)|D
// C型、もしくはX型かつD型、もしくはnull
C|(X&D)|null
// A型かつB型かつD型、もしくはint、もしくはnull
(A&B&D)|int|null
DNFでない形の複合型宣言はパースエラーになります。
その場合はDNFに書き替える必要があります。
A&(B|D)
// (A&B)|(A&D) と書き替える
A|(B&(D|W)|null)
// A|(B&D)|(B&W)|null と書き替える
セクション内の順番は無関係であり、以下の型宣言は全て等価です。
(A&B)|(C&D)|(Y&D)|null
(B&A)|null|(D&Y)|(C&D)
null|(C&D)|(B&A)|(Y&D)
DNFを採用した理由は、エンジンにとって解析が容易であり、人の目や静的解析にとっても理解しやすく、理論的にはあらゆる論理式をDNFで表すことが可能であるためです。
なお、DNF型においては&
を囲む括弧は必須です。
&
だけの交差型であれば括弧は必要ありません。
Return co-variance
返り値の共変性について。
extendsする場合、返り値は狭めることしかできません。
実用的には、ANDを追加することはできますが、ORを追加することはできない、ということになります。
// A型かつB型、もしくはD型
interface ITest {
public function stuff(): (A&B)|D;
}
// 狭まったのでOK
class TestOne implements ITest {
public function stuff(): (A&B) {}
}
// 狭まったのでOK
class TestTwo implements ITest {
public function stuff(): D {}
}
// OK CはA&Bより狭い
class TestThree implements ITest {
public function stuff(): C|D {}
}
// NG A型であり、B型でないものが通ってしまう
class TestFour implements ITest {
public function stuff(): A|D {}
}
interface ITestTwo {
public function things(): C|D {}
}
// NG A型かつB型であり、C型ではないものが通ってしまう
class TestFive implements ITestTwo {
public function things(): (A&B)|D {}
}
Parameter contra-variance
引数の反変性について。
extendsする場合、引数は広げることしかできません。
実用的には、ORを追加することはできますが、ANDを追加することはできない、ということになります。
// A型かつB型、もしくはD型
interface ITest {
public function stuff((A&B)|D $arg): void {}
}
// Z型まで広がったのでOK
class TestOne implements ITest {
public function stuff((A&B)|D|Z $arg): void {}
}
// A型まで広がったのでOK
class TestOne implements ITest {
public function stuff(A|D $arg): void {}
}
// NG D型が通らなくなった
class TestOne implements ITest {
public function stuff((A&B) $arg): void {}
}
interface ITestTwo {
public function things(C|D $arg): void;
}
// OK A型かつB型であり、C型ではないものまで広がった
class TestFive implements ITestTwo {
public function things((A&B)|D $arg): void;
}
Property invariance
プロパティの不変性について。
オブジェクトのプロパティの型は、継承元と同じでなければなりません。
このRFCでは、その仕様はそのままです。
ただし論理的に同一であれば、順序の入れ替わりは許容します。
Duplicate and redundant types
重複・冗長な型指定。
クラスを読み込まずに検出できる冗長な型指定はコンパイルエラーになります。
これは既存のUNION型・交差型に適用されているロジックと同等です。
DNFの各セグメントは一意である必要があります。
(A&B)|(B&A)
は不正な型です。
これは等価なので冗長です。
なお、(A&B)|C
は有効な型指定です。
AとBを含むC以外の型を作ることが可能です。
他のセグメントの部分集合であるセグメントは許可されません。
例として(A&B)|A
は、(A&B)
がA
の部分集合であるため無効です。
ただし、この判定は、この型が最小であることを保証するものではありません。
Reflection
このRFCでは、新しいReflectionクラスは導入されません。
ReflectionUnionType::getTypes()に変更が入ります。
返り値の定義はReflectionTypeの配列となっています。
実際はReflectionNamedTypeの配列を返しますが、これがReflectionUnionTypeとReflectionIntersectionTypeの配列を返すようになります。
メソッド定義は変更ありません。
Backward Incompatible Changes
互換性のない変更。
ReflectionUnionTypeの返り値は、これまでは必ずReflectionNamedTypeでしたが、ReflectionIntersectionTypeが入ってくることがあります。
Proposed PHP Version(s)
PHP8.2。
Proposed Voting Choices
投票期間は2022/06/17から2022/07/01まで。
このRFCは、賛成25反対1の圧倒的賛成多数で受理されました。
唯一の反対がDmitry Stogovなのはちょっと気になるところ。
※Dmitry StogovはPHP本体のcontribute数1位。
Future Scope
この項目は将来の展望であり、今回のRFCには含まれません。
Non-DNF types
DNFでない型のサポート。
理論的には、正規形でない型についても、コンパイル時の解析によってサポートすることは可能です。
しかし、DNFの時点で全ての型を表現できること、人間の目と解析エンジンいずれにとってもコードを単純化できることもあるため、著者としてはこちらを追求するつもりはありません。
Type aliasing
DNFの型表現はかなり長くなる可能性があります。
そのため、便利な表現である型エイリアスを導入する動機となるかもしれません。
Patches and Tests
感想
DNFの型表現ができるようになり、やろうと思えばPHP上で※ほぼ※あらゆる型を表現できるようになりました。
※resource型が表現できない
型?なにそれおいしいの?
という世界だった過去のPHPとは全く隔世の感がありますね。
ただ正直、個人的にはこの手の型パズルにいまいちメリットを感じられません。
これができるようになることで何がどう良くなるってのがさっぱりわからないんですよね。
まあでも、すごい人たちが熟考して導入を決めたわけですし、きっとすごい人たちがすごい使い方を教えてくれるに違いありません。
PHPの型の歴史
せっかくなのでPHPで使える型がどのように広がっていったのかを並べたものです。
PHP7以降、中でも8.0からの拡充っぷりがすごいですね。
PHP5.0
・クラス名 / self / parent
PHP5.0で引数の型宣言が初めて導入された。
PHP7まではタイプヒンティングと呼ばれていた。
function hoge(stdClass $class){}
PHP5.1
・array
function hoge(array $arr){}
PHP5.4
・callable
function hoge(callable $c){}
PHP7.0
・int / float / bool / string
基本型がようやく使用可能になった。
・返り値の型宣言
返り値の型が指定できるようになった。
function hoge(int $int):int{
return $int;
}
・strict_types
厳密な型宣言が可能になった。
PHPの型宣言は、デフォルトでは暗黙の型変換が働いてしまうが、それを無効にして型指定を厳密に適用することができる。
function hoge(int $i){}
hoge('1'); // 通る。1になる
declare(strict_types=1);
hoge('1'); // NG
PHP7.1
・iterable
foreach可能な値を表す。
function hoge(iterable $param){
foreach($param as $v){}
}
・null許容型
null許容型は、呼び出しの引数を省略することもできる。
function hoge(?int $i){}
hoge();
・void
返り値としてのみ使用可能。
値を返さないことを明示する。
function hoge():void{}
PHP7.2
・object
インスタンスであればなんでもいい型。
function hoge(object $i){}
hoge(new stdClass());
hoge(new DateTimeImmutable());
PHP8.0
・mixed
var_dumpの引数など、なんでもいい型。
引数を書いていないのではなく、不定ということを明示する。
・static
返り値としてのみ使用可能。
自分と同じクラスを返すことを明示する(厳密には異なるが面倒いので割愛)。
class A{
public function foo():static{
return new static();
}
}
・UNION型
型のOR。
型を組み合わせで表現できるようになった。
function hoge(int|stdClass $int_or_stdClass){}
・ false疑似型 / null疑似型
UNION型の要素としてのみ使用可能で、単独使用はできない疑似型。
null許容型?int
はUNION型int|null
の省略形という扱いになった。
function hoge(int|false|null $int_or_false_or_null){}
PHP8.1
・readonlyプロパティ
readonlyプロパティ自体は型宣言ではないが、readonlyを宣言したプロパティには型宣言が必須になる。
・交差型
型のAND。
UNION型と組み合わせての使用はできない。
function hoge(Traversable&Countable $Traversable_and_Countable){}
・never
返り値としてのみ使用可能。
呼び出し元に戻らないことを明示する。
function hoge():never{ exit; }
PHP8.2
・readonlyクラス
readonlyクラス自体は型宣言ではないが、readonlyを宣言したクラスのプロパティには型宣言が必須になる。
・false / null
falseとnullが疑似型から正式な型に昇格。
単独で使えるようになる。
function hoge(false $false){}
・true
false型に合わせてtrue型も使えるようになる。
function hoge(true $true){}
・選言標準形
UNION型と交差型を同時使用可能になる。
function hoge(int|(Traversable&Countable) $int_or_traversableandcountable){}