【PHP8.2】PHPで選言標準形 (Disjunctive Normal Form) 型が使えるようになる

[タイトルの意味](https://ja.wikipedia.org/wiki/%E9%81%B8%E8%A8%80%E6%A8%99%E6%BA%96%E5%BD%A2)はよくわからない。

さてPHPでは、[PHP8.0で型のOR](https://qiita.com/rana_kualu/items/f75e254553157d8eb0b7)、[PHP8.1で型のAND](https://qiita.com/rana_kualu/items/f49582bc94e0bae23d9c)が使えるようになりました。

しかし、この両者を組み合わせて使うことはできません。

```php
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](https://wiki.php.net/rfc/dnf_types)の日本語訳です。


# PHP RFC: Disjunctive Normal Form Types

## Introduction

[Disjunctive Normal Form](https://en.wikipedia.org/wiki/Disjunctive_normal_form) (DNF) とは、論理式を正規化する標準的な方法のひとつです。
具体的には、ANDをまとめたものをORで並べる形に構造化します。
これを型宣言に適用すると、PHPのパーサーが扱えるUNION型とIntersection型を組み合わせた書き方が可能になります。


## Proposal

### Examples

これ以降の例では、次の定義が存在するものとします。

```php
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で型宣言を行うことができるようになります。

```php
// 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に書き替える必要があります。

```php
A&(B|D)
// (A&B)|(A&D) と書き替える

A|(B&(D|W)|null)
// A|(B&D)|(B&W)|null と書き替える
```


セクション内の順番は無関係であり、以下の型宣言は全て等価です。

```php
(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を追加することはできない、ということになります。

```php
// 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を追加することはできない、ということになります。

```php
// 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()](https://www.php.net/manual/ja/reflectionuniontype.gettypes.php)に変更が入ります。
返り値の定義は[ReflectionType](https://www.php.net/manual/ja/class.reflectiontype.php)の配列となっています。
実際は[ReflectionNamedType](https://www.php.net/manual/ja/class.reflectionnamedtype.php)の配列を返しますが、これが[ReflectionUnionType](https://www.php.net/manual/ja/class.reflectionuniontype.php)と[ReflectionIntersectionType](https://www.php.net/manual/ja/class.reflectionintersectiontype.php)の配列を返すようになります。
メソッド定義は変更ありません。


## Backward Incompatible Changes

互換性のない変更。

[ReflectionUnionType](https://www.php.net/manual/ja/class.reflectionuniontype.php)の返り値は、これまでは必ず[ReflectionNamedType](https://www.php.net/manual/ja/class.reflectionnamedtype.php)でしたが、[ReflectionIntersectionType](https://www.php.net/manual/ja/class.reflectionintersectiontype.php)が入ってくることがあります。


## Proposed PHP Version(s)

PHP8.2。


## Proposed Voting Choices

投票期間は2022/06/17から2022/07/01まで。

このRFCは、賛成25反対1の圧倒的賛成多数で受理されました。

唯一の反対が[Dmitry Stogov](https://people.php.net/dmitry)なのはちょっと気になるところ。
※Dmitry Stogovは[PHP本体のcontribute数1位](https://github.com/php/php-src/graphs/contributors)。


## Future Scope

この項目は将来の展望であり、今回のRFCには含まれません。

### Non-DNF types

DNFでない型のサポート。

理論的には、正規形でない型についても、コンパイル時の解析によってサポートすることは可能です。
しかし、DNFの時点で全ての型を表現できること、人間の目と解析エンジンいずれにとってもコードを単純化できることもあるため、著者としてはこちらを追求するつもりはありません。

### Type aliasing

DNFの型表現はかなり長くなる可能性があります。
そのため、便利な表現である型エイリアスを導入する動機となるかもしれません。

## Patches and Tests

[プルリクエスト](https://github.com/php/php-src/pull/8725)。



# 感想

DNFの型表現ができるようになり、やろうと思えばPHP上で※ほぼ※あらゆる型を表現できるようになりました。
 ※resource型が表現できない

型？なにそれおいしいの？
という世界だった過去のPHPとは全く隔世の感がありますね。

ただ正直、個人的にはこの手の型パズルにいまいちメリットを感じられません。
これができるようになることで何がどう良くなるってのがさっぱりわからないんですよね。

まあでも、すごい人たちが熟考して導入を決めたわけですし、きっとすごい人たちがすごい使い方を教えてくれるに違いありません。


# PHPの型の歴史

せっかくなのでPHPで使える型がどのように広がっていったのかを並べたものです。

PHP7以降、中でも8.0からの拡充っぷりがすごいですね。

## PHP5.0

・クラス名 / self / parent

PHP5.0で引数の型宣言が初めて導入された。
PHP7まではタイプヒンティングと呼ばれていた。

```php
function hoge(stdClass $class){}
```

## PHP5.1

・array

```php
function hoge(array $arr){}
```

## PHP5.4

・callable

```php
function hoge(callable $c){}
```

## PHP7.0

・int / float / bool / string

基本型がようやく使用可能になった。

・返り値の型宣言

返り値の型が指定できるようになった。

```php
function hoge(int $int):int{
	return $int;
}
```

・strict_types

[厳密な型宣言](https://www.php.net/manual/ja/language.types.declarations.php#language.types.declarations.strict)が可能になった。
PHPの型宣言は、デフォルトでは暗黙の型変換が働いてしまうが、それを無効にして型指定を厳密に適用することができる。

```php
function hoge(int $i){}
hoge('1'); // 通る。1になる

declare(strict_types=1);
hoge('1'); // NG
```


## PHP7.1

・iterable

foreach可能な値を表す。

```php
function hoge(iterable $param){
  foreach($param as $v){}
}
```

・null許容型

null許容型は、呼び出しの引数を省略することもできる。

```php
function hoge(?int $i){}
hoge();
```

・void

返り値としてのみ使用可能。
値を返さないことを明示する。

```php
function hoge():void{}
```

## PHP7.2

・object

インスタンスであればなんでもいい型。

```php
function hoge(object $i){}
hoge(new stdClass());
hoge(new DateTimeImmutable());
```

## PHP8.0

・mixed

[var_dump](https://www.php.net/manual/ja/function.var-dump.php)の引数など、なんでもいい型。
引数を書いていないのではなく、不定ということを明示する。

・static

返り値としてのみ使用可能。
自分と同じクラスを返すことを明示する(厳密には異なるが面倒いので割愛)。

```php
class A{
    public function foo():static{
        return new static();
    }
}
```


・UNION型

[型のOR](https://qiita.com/rana_kualu/items/f75e254553157d8eb0b7)。
型を組み合わせで表現できるようになった。

```php
function hoge(int|stdClass $int_or_stdClass){}
```

・ false疑似型 / null疑似型

UNION型の要素としてのみ使用可能で、単独使用はできない疑似型。
null許容型`?int`はUNION型`int|null`の省略形という扱いになった。

```php
function hoge(int|false|null $int_or_false_or_null){}
```

## PHP8.1

・readonlyプロパティ

[readonlyプロパティ](https://qiita.com/rana_kualu/items/ae96400a0a6981eca64a)自体は型宣言ではないが、readonlyを宣言したプロパティには型宣言が必須になる。


・交差型

[型のAND](https://qiita.com/rana_kualu/items/f49582bc94e0bae23d9c)。
UNION型と組み合わせての使用はできない。

```php
function hoge(Traversable&Countable $Traversable_and_Countable){}
```

・never

返り値としてのみ使用可能。
呼び出し元に戻らないことを明示する。

```php
function hoge():never{ exit; }
```


## PHP8.2

・readonlyクラス

[readonlyクラス](https://qiita.com/rana_kualu/items/0d8cfb5d0f83ae24f943)自体は型宣言ではないが、readonlyを宣言したクラスのプロパティには型宣言が必須になる。


・false / null

falseとnullが疑似型から正式な型に昇格。
[単独で使えるようになる](https://qiita.com/rana_kualu/items/2ef15cfa2dea8ba0f061)。

```php
function hoge(false $false){}
```

・true

false型に合わせて[true型](https://qiita.com/rana_kualu/items/2e64ecb8740d21f4fd34)も使えるようになる。

```php
function hoge(true $true){}
```

・選言標準形

UNION型と交差型を同時使用可能になる。

```php
function hoge(int|(Traversable&Countable) $int_or_traversableandcountable){}
```