【PHP8.0】PHPでunion型が使えるようになる

[Union Types 2.0](https://wiki.php.net/rfc/union_types_v2)というRFCが投票中です。
提案者はまたまたのNikita。
2019/10/25開始、2019/11/08終了で、受理には2/3+1の賛成が必要です。
2019/11/04時点で賛成55反対5であり、ほぼ導入確定です。

PHPのunion型って何かというと、[TypeScriptのunion型](https://qiita.com/uhyo/items/e2fdef2d3236b9bfe74a#union%E5%9E%8B%E5%90%88%E4%BD%B5%E5%9E%8B)とだいたい同じです。
`int|string $a`と書いたら`$a`は`int型`もしくは`string型`ですよ、ということです。

ちなみに別途RFCをGitHubで管理しようという実験が進行中で、このRFCの詳細は[GitHub上に存在します](https://github.com/nikic/php-rfcs/blob/union-types/rfcs/0000-union-types-v2.md)。
このRFCはまだNikitaの個人GitHub上にしかないのですが、本決まりになったら[PHP公式](https://github.com/php/php-rfcs)に移動になると思います。
まあGitHubのほうが管理とか更新とか楽ですからね。
ただGitHubはURLがすぐ404になるのだけはどうにかしてほしい。


# Union Types 2.0

## Introduction

union型は、単一の型ではなく、複数の異なる型の値を受け入れます。
PHPは既に、2種類の特別なunion型をサポートしています。

・null許容型。`?T`で使える。
・array型もしくはTraversable型。`iterable`で使える。

しかし任意のunion型はサポートされていません。
現在はかわりにphpdoc注釈を書くくらいしかできません。

```php
class Number {
    /**
     * @var int|float $number
     */
    private $number;

    /**
     * @param int|float $number
     */
    public function setNumber($number) {
        $this->number = $number;
    }

    /**
     * @return int|float
     */
    public function getNumber() {
        return $this->number;
    }
}
```

Statisticsセクションで、オープンソースでunion型がどれだけ普及しているかを示しています。

言語でunion型をサポートすることにより、より多くの型情報をphpdocに頼ることなく関数シグナチャに移動することができ、多数の利点が得られます。

・型は実際に強制されるため、ミスを早期に発見できる。
・エッジケースを見逃したり、仕様変更の際にドキュメントを更新し忘れたりする可能性が減る。
・継承時にもリスコフの置換原則を適用できる。
・リフレクションから利用できる。
・phpdocより分量が減らせる。

union型は、ジェネリクスと並んで型システムに残っている最大の穴です。


## Proposal

union型を構文`T1|T2|...`で表し、型を書くことができる全ての位置でunion型を使用可能とする。

```php
class Number {
    private int|float $number;

    public function setNumber(int|float $number): void {
        $this->number = $number;
    }

    public function getNumber(): int|float {
        return $this->number;
    }
}
```


### Supported Types

union型は、現在PHPでサポートされている全ての型をサポートしますが、一部の型については注意が必要です。

#### void型

void型は、union型の一部となることは決してできません。
`T|void`のような型は、戻り値を含む全ての位置で不正です。

void型は関数に戻り値がないことを表します。
あらゆる非void型と互換がありません。

かわりにnull許容型`?T`があり、これはT型もしくはnullを返すことができます。


#### null許容型

`T1|T2|null`としてnullを許容するunion型を定義することができます。
既存の`?T`は、`T|null`の省略形と見做されます。

このRFCの草稿では、null許容型の文法が2種類になることを防ぐため、`?(T1|T2)`という文法を提唱していました。
しかしこの構文は都合が悪く、さらにphpdocで確立されている`T1|T2|null`構文とも異なっています。
議論の結果は、`T1|T2|null`の文法が圧倒的に優勢でした。
`?T`は`T|null`の省略形として今後も有効な構文で、推奨も非推奨もされず、当分は廃止される予定もありません。

null型は、union型の一部としてのみ有効な型で、単独で使うことはできません。

union型と`?T`表記を混ぜて使用することはできません。
`?T1|T2`、`T1|?T2`、`?(T1|T2)`は全て不正な文法で、この場合は`T1|T2|null`を使う必要があります。

#### false疑似型

現在では、エラーや不正が起きた際の関数の戻り値は`null`にすることが推奨されていますが、歴史的理由から多くの内部関数は`false`を返してきます。
Statisticsセクションで示すように、union型を返す内部関数は大部分がfalseを含んでいます。

一般的な例としては`int|false`を返す[strpos](https://www.php.net/manual/ja/function.strpos.php)などです。
これを正しく表記すると`int|bool`ですが、これは関数が`true`を返すこともあるという誤った印象を与えます。

そのため、このRFCには`false`のみを表す`false疑似型`が含まれています。
`true`のみを表す`true疑似型`は、それが必要となる歴史的理由が存在しないため、このRFCには含まれません。

`false疑似型`は、union型の一部としてのみ有効で、単独やnull許容型として使うことはできません。
`false`、`false|null`、`?false`は全て無効な構文です。


#### Duplicate and redundant types

クラスのロードを行わずに検出できる冗長な記述は、コンパイルエラーになります。
これは以下のような例を含みます。

・同じ型の重複。`int|string|INT`は不可。
・`bool`には`false`を追加できない。
・`object`には個別のクラス型を追加できない。
・`iterable`には`array`と`Traversable`を追加できない。

これは、型が最小であることを保証はしません。
たとえばクラスAとBがクラスエイリアスや継承関係にある場合、`A|B`は有効です。

```php
function foo(): int|INT {} // ×
function foo(): bool|false {} // ×

use A as B;
function foo(): A|B {} // × 構文解析時点でわかる

class_alias('X', 'Y');
function foo(): X|Y {} // 許可 実行時までわからない
```

#### Type grammar

特殊なvoid型を除くと、型の構文は以下のようになります。

```
type: simple_type
    | "?" simple_type
    | union_type
    ;

union_type: simple_type "|" simple_type
          | union_type "|" simple_type
          ;

simple_type: "false"          # union型でのみ有効
           | "null"           # union型でのみ有効
           | "bool"
           | "int"
           | "float"
           | "string"
           | "array"
           | "object"
           | "iterable"
           | "callable"       # プロパティ型指定では無効
           | "self"
           | "parent"
           | namespaced_name
           ;
```

### Variance

union型は、既存の型ルールに従います。
・戻り値は共変。
・パラメータ型は反変。
・プロパティ型は不変。

唯一の変更点は、union型が派生型と相互作用する点であり、そのため3つの追加ルールがあります。

・全ての`U_i`が`V_j`のサブタイプであった場合、`U_1|...|U_n`は`V_1|...|V_m`のサブタイプである。
・`iterable`は`array|Traversable`と同じと見做す。
・false疑似型は`bool`のサブタイプと見做す。

以下において、許可されているものと許可されないものの例を幾つか示します。

#### Property types

プロパティの型は不変です。
すなわち、継承しても型は同じである必要があります。
ただし、この"同じ"は意味が同じということを表します。
これまでもクラスエイリアスで同じクラスを表す別名を付けることができました。

union型はこの"同じ"の範囲を広げ、たとえば`int|string`と`string|int`は同じとして扱います。

```php
class A {}
class B extends A {}

class Test {
    public A|B $prop;
}
class Test2 extends Test {
    public A $prop;
}
```

この例では、親クラスの`A|B`型と子クラスの`A`型は明らかに異なる型であるにもかかわらず、これは正当な文法です。
内部的には、以下のようにこの結果に到達します。
まず、それは`A`のサブタイプであるため、`A`は`A|B`のサブタイプです。[^1]
次に`A`は`A`のサブタイプであり、`B`は`A`のサブタイプであるため、`A|B`は`A`のサブタイプです。


#### Adding and removing union types

戻り値からunion型の一部を削除し、パラメータに一部の型を追加することは正しい文法です。

```php
class Test {
    public function param1(int $param) {}
    public function param2(int|float $param) {}

    public function return1(): int|float {}
    public function return2(): int {}
}

class Test2 extends Test {
    public function param1(int|float $param) {} // OK: パラメータの型追加は許可
    public function param2(int $param) {}       // NG: パラメータの型削除は禁止

    public function return1(): int {}           // OK: 返り値の型削除は許可
    public function return2(): int|float {}     // NG: 返り値の型追加は禁止
}
```


#### Variance of individual union members

同様に、戻り値の型を狭めたり、パラメータの型を広げることは許可されます。

```php
class A {}
class B extends A {}

class Test {
    public function param1(B|string $param) {}
    public function param2(A|string $param) {}

    public function return1(): A|string {}
    public function return2(): B|string {}
}

class Test2 extends Test {
    public function param1(A|string $param) {} // OK: BをAに広げた
    public function param2(B|string $param) {} // NG: AをBに狭めた

    public function return1(): B|string {}     // OK: AをBに狭めた
    public function return2(): A|string {}     // NG: BをAに広げた
}
```

もちろん同じことを複数のunion型に同時に行ったり、型の追加削除と型の拡縮を組み合わせることもできます。


#### Coercive typing mode

`strict_types`が有効でない場合、スカラー型宣言は[暗黙の型変換](https://qiita.com/rana_kualu/items/9b4502d229d19635ef34#strict_types%E3%81%AE%E5%BD%B1%E9%9F%BF)の対象となります。
これは一部のunion型において、変更先の型を一意に決められないため問題となります。
たとえば`int|string`に`false`を渡すと、`0`と`""`の両方が暗黙の型変換の候補になります。

従って、引数に正しくないスカラー値が渡ってきた場合、以下の優先順位で変換することにします。

1.int
2.float
3.string
4.bool

PHPの既存の型変換機構で型変換が可能である場合、その型が選ばれます。

例外として、値が文字列であり、union型が`int|float`である場合、優先される型は引数の数値文字列の中身によって決まります。
すなわち、"42"はint型となり、"42.0"はfloat型となります。

上記のリストに含まれない型には自動型変換されません。
特に`null`や`false`への自動型変換は起きないことに注意しましょう。

```php
// int|string
42    --> 42          // 正しい型
"42"  --> "42"        // 正しい型
new ObjectWithToString --> "__toString()の結果" // objectはint型にならない
42.0  --> 42          // floatはint型になる
42.1  --> 42          // floatはint型になる
1e100 --> "1.0E+100"  // floatの上限を超えたらstring型になる
INF   --> "INF"       // floatの上限を超えたらstring型になる
true  --> 1           // boolはint型になる
[]    --> TypeError   // 配列はint|string型にならない

// int|float|bool
"45"    --> 45        // 整数っぽいのでint型になる
"45.0"  --> 45.0      // 小数っぽいのでfloat型になる
"45X"   --> 45 + Notice: Non well formed numeric string // 有効部分は整数っぽいのでint型になり、E_NOTICEが出る
""      --> false     // 数値形式文字列でないのでbool型になる
"X"     --> true      // 数値形式文字列でないのでbool型になる
[]      --> TypeError // 配列はint|float|bool型にならない
```


#### Alternatives

自動型変換については、別案が2種類ありました。

ひとつめはunion型は常に厳密な型指定とすることで、複雑な強制型変換を完全に排除することです。
これは2つの欠点があります。
まず、strictでないときに型を`float`から`float|int`にすると、直感に反して有効な入力が減ります。
第二に、`float型`が`float|int型`のサブタイプと言えなくなるため、union型のモデルが崩壊します。

二つ目が、変換の優先順位を型の順番にすることです。
これは即ち`int|string`と`string|int`が異なる型になることを意味します。
この変換は直感的ではなく、継承関係に非常に不明瞭な影響を及ぼします。


### Property types and references

union型プロパティへの参照は、[プロパティ型指定RFC](https://qiita.com/rana_kualu/items/9b4502d229d19635ef34)に書かれた挙動に従います。
プロパティ型指定とunion型の組み合わせによる影響は、[当時から考慮されていました](https://wiki.php.net/rfc/typed_properties_v2#future_interaction_with_union_types)。

```php
class Test {
    public int|string $x;
    public float|string $y;
}
$test = new Test;
$r = "foobar";
$test->x =& $r;
$test->y =& $r;

// $rと$test->xと$test->yは同じもので、型は{ mixed, int|string, float|string }の論理積になる

$r = 42; // TypeError
```

複数のリファレンスが紐付けられている場合、型の強制変換が行われた後の最終的な型は全ての型と互換する必要があります。
上記例の場合、`$test->x`はint型の`42`になり、`$test->y`はfloat型の`42.0`になります。
これは同じ型ではないため、TypeErrorが投げられます。

この場合は共通の型であるstring型にキャストすることでエラーは出なくなりますが、自動型変換による優先順位と異なるため、型がどうなるかわからないという欠点があります。


### Reflection

union型をサポートする`ReflectionUnionType`クラスが追加されます。

```php
class ReflectionUnionType 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`クラスの配列を返します。
この型は、元の型宣言と順番が異なる可能性があり、また等価である別の型になる可能性があります。

たとえば`int|string`型が`["string", "int"]`の順で要素を返す場合があります。
また`iterable|array|string`型は`["iterable", "string"]`になるかもしれないし`["Traversable", "array", "string"]`になるかもしれません。
Reflection APIが保証するのは、論理的に同じものであるということです。

`allowsNull()`メソッドは、union型の要素に`null型`が含まれるか否かを返します。

`__toString()`メソッドは、型宣言を有効なコード表現として返します。
元の型宣言と必ずしも同じではありません。

後方互換性のため、null許容型`?T`、`T|null`のunion型は、`ReflectionUnionType`ではなく`ReflectionNamedType`を返します。

```php
	// getTypes()や__toString()の結果は順番が異なることもある

	function test(): float|int {}
	$rt = (new ReflectionFunction('test'))->getReturnType();
	var_dump(get_class($rt));    // "ReflectionUnionType"
	var_dump($rt->allowsNull()); // false
	var_dump($rt->getTypes());   // [ReflectionType("int"), ReflectionType("float")]
	var_dump((string) $rt);      // "int|float"

	function test2(): float|int|null {}
	$rt = (new ReflectionFunction('test2'))->getReturnType();
	var_dump(get_class($rt));    // "ReflectionUnionType"
	var_dump($rt->allowsNull()); // true
	var_dump($rt->getTypes());   // [ReflectionType("int"), ReflectionType("float"), ReflectionType("null")]
	var_dump((string) $rt); // "int|float|null"

	function test3(): int|null {}
	$rt = (new ReflectionFunction('test3'))->getReturnType();
	var_dump(get_class($rt));    // "ReflectionNamedType"
	var_dump($rt->allowsNull()); // true
	var_dump($rt->getName());    // "int"
	var_dump((string) $rt);      // "?int"
```


## Backwards Incompatible Changes

このRFCには、後方互換性のない変更はありません。
ただし`ReflectionType`を利用しているコードは、union型に関する処理を追加する必要があります。


## Future Scope

この項目は今後の展望であり、このRFCには含まれていません。

### Intersection Types

[交差型](https://qiita.com/rana_kualu/items/8337e5ead7d52badf3e1#%E8%8F%B1%E5%BD%A2%E7%B6%99%E6%89%BF)は論理的にunion型と似ています。
union型では少なくとも一つの型が満たされる必要がありますが、交差型では全ての型が満たされる必要があります。

たとえば`Traversable|Countable`は`Traversable`と`Countable`のうち少なくともどちらかである必要がありますが、`Traversable&Countable`は`Traversable`でありなおかつ`Countable`である必要があります。

### Mixed Type

`mixed型`は、任意の値が受け入れ可能であることを表します。
型指定が無い場合と見た目の動きは同じですが、型指定が無いと、それが本当に自由であるのか、単に型指定を書き忘れただけなのかが区別できません。

### Literal Types

このRFCで導入された`false疑似型`は、[TypeScript](https://www.typescriptlang.org/docs/handbook/advanced-types.html#string-literal-types)でサポートされている[リテラル型](https://qiita.com/uhyo/items/e2fdef2d3236b9bfe74a#%E3%83%AA%E3%83%86%E3%83%A9%E3%83%AB%E5%9E%8B)の特殊なケースです。
リテラル型は、列挙型のように一部特定の値のみを許可できる型です。

```php
type ArrayFilterFlags = 0|ARRAY_FILTER_USE_KEY|ARRAY_FILTER_USE_BOTH;
array_filter(array $array, callable $callback, ArrayFilterFlags $flag): array;
```

列挙型ではなくリテラル型を使用する利点は、元の文法をそのまま維持できることです。
そのため、後方互換性を壊すことなく後付けすることができます。


### Type Aliases

型が複雑になると、型宣言の再利用が必要になります。
その一般的な方法は2種類が考えられます。
ひとつめは次のようなエイリアスです。

```php
use int|float as number;

function foo(number $x) {}
```

この`number型`はソースコード上でのみ現れる型で、コンパイル時に元の`int|float`に解決されます。

もうひとつは型宣言を定義することです。

```php
namespace Foo;
type number = int|float;

// \Foo\numberをどこからでも使える
```


## Statistics

上位2000パッケージの`@param`と`@return`において、野生のunion型がどれだけ使われているかを分析しました。

・`@param`のunion型：25k。[一覧](https://gist.github.com/nikic/64ff90c5038522606643eac1259a9dae#file-param_union_types-json)
・`@return`のunion型：14k。[一覧](https://gist.github.com/nikic/64ff90c5038522606643eac1259a9dae#file-return_union_types-json)

PHPの内部関数を調べたところ(調査が不完全なので最低2倍はあるはず)

・336関数がunion型を返す。
・そのうち213関数がfalse型を返す。

多くの内部関数が、戻り値の型を表すために`false疑似型`が必要であることを示しています。


# 感想

元々型に厳密なTypeScriptがunion型を導入する理由はまあわかるんですよ。
しかしですね、元々フリーダム型だったPHPがわざわざ[型宣言](https://qiita.com/hikaru_/items/2c12068eafea75e51cce)やら[プロパティ型指定](https://qiita.com/rana_kualu/items/9b4502d229d19635ef34)やらで狭めてきた上でのunion型って、なんというかこうマッチポンプ感とかそんな感じを感じざるをえない。
いやまあ違う話だってのは理屈ではわかるんですけど感覚的にね。

PHPでunion型をうまく使う方法、正直あまり思いつきません。
誰かがきっといいサンプルを考えてくれるはず。


[^1]: `it`がどれにかかってるのかわからなかった。というかここの文の意味がわからん。