交差型は何かって一言で言うと型のANDです。
PHP8.0で型のORことUNION型が導入されましたが、これに続いてPHP8.1で交差型が導入されることになりました。
これでPHPでも型パズルマウント取れるようになりますね。
ということで以下はPure intersection typesの紹介です。
PHP RFC: Pure intersection types
Introduction
交差型とは、型がひとつの制約ではなく、複数の制約を同時に満たすことを必要とする型です。
交差型は、現在のところ言語ネイティブには対応していません。
かわりにphpdocアノテーションを使用したり、型付きプロパティを濫用したりしています。
class Test {
private ?Traversable $traversable = null;
private ?Countable $countable = null;
/** @var Traversable&Countable */
private $both = null;
public function __construct($countableIterator) {
$this->traversable =& $this->both;
$this->countable =& $this->both;
$this->both = $countableIterator;
}
}
交差型をPHP本体でサポートすることにより、型情報を関数のシグネチャに記述することが可能となり、多くの利点が生まれます。
・型が実際に強制されるため、誤りを早期に発見できる。
・型が古くなったり、エッジケースを見逃したりする可能性が低くなる。
・型は継承時にチェックされ、リスコフの置換原則が適用される。
・Reflectionから交差型を使用可能。
・構文はphpdocよりシンプルになる。
Motivation
複数のインターフェイスを継承した新しいインターフェイスを作成することで、交差型を疑似再現することは可能です。
そのような例として内蔵のSeekableIteratorが存在します。
これはIteratorインターフェイスを継承してseek()
メソッドを追加したものです。
ところで新たにCountできるイテレータがほしいと思った場合、新たなインターフェイスを作成する必要があります。
interface CountableIterator extends Iterator, Countable {}
たしかにこれは動作します。
さて、それではcountできてシークもできるイテレータがほしくなったときはどうすればよいでしょう。
interface SeekableCountableIterator extends CountableIterator, SeekableIterator {}
このように、新しい要求があるたびに、全ての可能な組み合わせを考慮しながら次々と新しいインターフェイスを作成していかなければなりません。
交差型はこれらの問題を解決します。
Proposal
現在型を書くことのできる全ての位置において、T1&T2&...
という構文で交差型をサポートします。
class A {
private Traversable&Countable $countableIterator;
public function setIterator(Traversable&Countable $countableIterator): void {
$this->countableIterator = $countableIterator;
}
public function getIterator(): Traversable&Countable {
return $this->countableIterator;
}
}
交差型とUNION型の混在、A&B|C
のような構文は、このRFCではサポートしません。
Supported types
交差型においてサポートされているのは、クラスタイプの型、すなわちインターフェイスとクラスです。
プリミティブ型の交差型は、たとえばint&string
のように、ほぼ全ての場合において値の無い型になるため対応しません。
またmixed型は、mixed&T
はただのT型であり、意味がないので対応されません。
iterable
疑似型の交差型は、iterable&T
はTraversable&T
と同じであるため、対応する必要がありません。
iterable&T = (array|Traversable)&T = (array&T) | (Traversable&T) = Traversable&T
callable
型の交差型は意味があることもあります(例:string&callable
)が、あまり賢明な使用法とは言えないでしょう。
parent
・self
・static
の交差型は技術的には可能ですが、親クラスが反するような奇妙な制限を子クラスに課したり、あるいは単に冗長なだけになってしまいます。
従って、これらの型を使った交差型は設計的に問題である可能性が高いため、禁止されます。
Duplicate and redundant types
クラスの読み込みを行わずに検出できる冗長な交差型は、コンパイル時にエラーになります。
A&B&A
のような同じクラスの交差型は禁止されます。
ただし、これは型が最小であることを保証はしません。
例として、BクラスがAクラスを継承していた場合のA&B
はB
ですが、これは許可されます。
function foo(): A&A {} // 禁止
use A as B;
function foo(): A&B {} // 禁止 パース時にわかる
class_alias('X', 'Y');
function foo(): X&Y {} // 許可 実行時までわからない
Type grammar
パーサーがリファレンス渡しの&
と交差型の&
を区別できるように、レキサーは&
の後に変数が続くか否かによって異なるトークンを生成するようになります。
結果として、型の文法は以下のようになります。
type_expr:
type
| '?' type
| union_type
| intersection_type
;
intersection_type:
type T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG type
| intersection_type T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG type
;
Variance
交差型は、既に継承や型チェックで使われている標準的なルールに従います。
・返り値の型は共変です。
・引数の型は反変です。
・プロパティの型は不変です。
交差型とサブタイプの相互作用により、ふたつのルールが追加されます。
・全てのB_i
に対してAがB_i
のサブタイプである場合、AはB_1&...&B_n
のサブタイプです。
・BのサブタイプA_i
が存在する場合、A_1&...&A_n
はBのサブタイプです。
どのような型が許され、何が許されないのか、以下にいくつかの例を挙げます。
Property types
プロパティの型は不変です。
すなわち継承前後で同じでなければなりません。
しかし、同じ型を複数の方法で表現することが可能です。
交差型はこの表現の幅を広げます。
たとえばA&B
とB&A
は同じ型です。
以下はより複雑な例です。
class A {}
class B extends A {}
class Test {
public A&B $prop;
}
class Test2 extends Test {
public B $prop;
}
この例では、交差型A&B
は実質的にB
であり等しくなるため、この構文は有効です。
Adding and removing intersection types
返り値への交差型の追加、および引数からの交差型の削除は合法です。
class A {}
interface X {}
class Test {
public function param1(A $param) {}
public function param2(A&X $param) {}
public function return1(): A&X {}
public function return2(): A {}
}
class Test2 extends Test {
public function param1(A&X $param) {} // NG
public function param2(A $param) {} // 有効
public function return1(): A {} // NG
public function return2(): A&X {} // 有効
}
Variance of individual intersection members
同様に、返り値の交差型の制限を狭めること、および引数の交差型の制限を広げることも有効です。
class A {}
class B extends A {}
interface X {}
class Test {
public function param1(B&X $param) {}
public function param2(A&X $param) {}
public function return1(): A&X {}
public function return2(): B&X {}
}
class Test2 extends Test {
public function param1(A&X $param) {} // 有効
public function param2(B&X $param) {} // NG
public function return1(): B&X {} // 有効
public function return2(): A&X {} // NG
}
もちろん、交差型の追加、削除と制限の拡縮を組み合わせることも可能です。
Variance of intersection type to concrete class type
交差型の主な用途は、複数のインターフェイスを確実に実装することなので、交差型の全てのインターフェイスを継承している具象クラスおよびインターフェイスはサブタイプとみなされます。
interface X {}
interface Y {}
class TestOne implements X, Y {}
interface A
{
public function foo(): X&Y;
}
interface B extends A
{
public function foo(): TestOne;
}
また具象クラスおよびインターフェイスのUNION型は、UNION型の各メンバーが交差型の全てのインターフェイスを実装している場合にのみ可能です。
class TestTwo implements X, Y {}
interface C extends A
{
public function foo(X&Y $param): TestOne|TestTwo;
}
Coercive typing mode
プリミティブ型は交差型に対応していないため、暗黙の型変換についての考慮は不要です。
Property types and references
交差型プロパティのリファレンスについては、型付きプロパティのRFCのセマンティクスに従います。
interface X {}
interface Y {}
interface Z {}
class A implements X, Y, Z {}
class B implements X, Y {}
class Test {
public X&Y $y;
public X&Z $z;
}
$test = new Test;
$r = new A;
$test->y =& $r;
$test->z =& $r;
// Reference set: { $r, $test->y, $test->z }
// Types: { A, X&Y, X&Z }
$r = new B; // TypeError: Cannot assign B to reference held by property Test::$z of type X&Z
Reflection
交差型に対応するリフレクションクラスReflectionIntersectionType
が追加されます。
class ReflectionIntersectionType extends ReflectionType {
/** @return ReflectionType[] */
public function getTypes();
/* Inherited from ReflectionType */
/** @return bool */
public function allowsNull();
/* Inherited from ReflectionType */
/** @return string */
public function __toString();
}
getTypes()
は、交差型の要素であるReflectionType
クラスの配列を返します。
元の型宣言と順番が異なる可能性があり、また等価である別の型になる可能性があります。
たとえばX&Y
が["Y", "X"]
の順番になる可能性もあります。
Reflection APIが保証するのは、論理的に同じものであるということです。
__toString()メソッドは、型宣言を有効なコード表現として返します。
元の型宣言と必ずしも同じではありません。
// 配列の順番は逆転する可能性があります。
function test(): A&B {}
$rt = (new ReflectionFunction('test'))->getReturnType();
var_dump(get_class($rt)); // "ReflectionIntersectionType"
var_dump($rt->allowsNull()); // false
var_dump($rt->getTypes()); // [ReflectionType("A"), ReflectionType("B")]
var_dump((string) $rt); // "A&B"
function test2(): A&B&C {}
$rt = (new ReflectionFunction('test2'))->getReturnType();
var_dump(get_class($rt)); // "ReflectionIntersectionType"
var_dump($rt->allowsNull()); // false
var_dump($rt->getTypes()); // [ReflectionType("A"), ReflectionType("B"), ReflectionType("C")]
var_dump((string) $rt); // "A&B&C"
Backward Incompatible Changes
このRFCには、後方互換性のない変更はありません。
ただしReflectionTypeを利用しているコードは、交差型に関する処理を追加する必要があります。
Proposed PHP Version
PHP8.1。
Future Scope
この項目は今後の展望であり、このRFCには含まれていません。
Composite types (i.e. mixing union and intersection types)
グループ化しないA&B|C
のような複合型のサポート。
Reflectionなど多くの考慮事項があり、特に継承時のルールやチェックが困難になります。
また、明示的にグループ化すべきだという意見もあります。
Type Aliases
型がどんどん複雑になっていく中で、型宣言の再利用を可能にすることに価値があるかもしれません。
その一般的な方法は2種類が考えられます。
ひとつめはエイリアスです。
use Traversable&Countable as CountableIterator;
function foo(CountableIterator $x) {}
この場合、CountableIterator
はローカルにのみ現れる型で、コンパイル時に元のTraversable&Countable
に解決されます。
もうひとつは型宣言の定義です。
namespace Foo;
type CountableIterator = Traversable&Countable;
// どこからでも\Foo\CountableIteratorを使える
これらをサポートした場合、複合型がサポートされているかのように型を書くことが可能になるため、注意が必要です。
Proposed Voting Choices
投票期間は2021/06/03から2021/06/17で、受理には投票の2/3の賛成が必要です。
2021/06/07時点では賛成15反対2の賛成多数であり、このまま順調にいけば受理されます。
Patches and Tests
プルリクエスト: https://github.com/php/php-src/pull/6799
感想
ここまできたのならもう型宣言もくれ。
と言いたいのですが本文にもあるように、一見普通の型宣言に見えて実は非対応ですって文が作れてしまうので、現状そのままでの導入は難しそうです。
type C = A|B;
function X(C&D){}
type E = string;
function X(E&F){}
このあたりについてはPHP8.2とかその後あたりできっとどうにかしてくれるでしょう。
ということでPHPはUNION型と交差型を手に入れ、並み居る言語の中でもトップクラスに高度な型操作が可能になります。
PHPには型が無いなんて言ってる連中は時代に取り残されているだけなので、みんなはぱりぱり型を使ったコードを書いていきましょう。
VSCodeなど統合開発環境を使っていれば、C#
並みに型を追跡してしてくれたりするのでリーディングもリファクタリングもとっても楽になります。
まあPHPの場合、型宣言を全部取っ払っちゃったりしても動作するのがいいところであり悪いところであるわけですが。