【PHP8.2】false疑似型およびnull型が単独で使えるようになる


PHP8.0において実装されたunion型の導入過程において、false疑似型およびnull型が導入されました。
これらはunion型の一部としてのみ有効な型であり、単独で使用することはできません。

しかし、これを単独で使えるようにしようというRFCが提出されました。
投票期間は2022/03/12から2022/03/26であり、賛成38反対0の全会一致で可決されました。
従ってPHP8.2から単独型として使用可能になります。

以下は該当のRFC、Allow null and false as stand-alone typesの日本語訳です。

Allow null and false as stand-alone types

Introduction

nullはPHPのUnit型です。
falseはbool型のリテラルです。

現在nullを単独で型宣言として使うことはできません。
nullはUnit型であるという性質上、いかなる情報も保持することはできないためです。

ところで関数がエラーであることを示すために、返り値として歴史的にfalseが使われてきました。
これがUNION型においてfalse疑似型が導入された大きな理由です。

Motivation

このRFCの動機です。

Type system completeness

型システムの完全性。

PHPは、PHP8.0でトップ型のmixed型を、PHP8.1でボトム型のnever型をサポートしました。
さらにPHP8.0でUNION型、PHP8.1で交差型という複合型もサポートしました。

PHPでUnit型を型付けできないのは、PHPに残されたわずかな欠陥です。

Edge case with regards to the literal type false

false疑似型のエッジケース。

false疑似型はUNION型でのみ使用できますが、null|falseは許されないため完全ではありません。
現在のところ、このエッジケースはbool|nullと表現するしかありません。
これはtrueを返すかもしれないという誤った印象を与えるため、人間や静的解析において有益ではありません。

PHPの組込関数にもnull|falseを必要とするものがあり、一例としてはgmp_random_seedです。

Providing precise type information while satisfying LSP

LSPを満たしたうえで正確な型情報を提供する。

親クラスでpublic function foo(): ?Tが定義されている場合に、子クラスでは必ずTを返すのであればpublic function foo(): Tと、より正確な型情報を返すことができます。
しかし逆はできません。
子クラスが必ずnullを返す場合は、public function foo(): nullとすることはできません。
この場合はシグネチャは元のままとし、返り値はドキュメントなどで示すしかありません。

PHPの組込関数ではSplFileObject::getChildren()等が該当します。

Proposal

型宣言が許される位置全てにおいて、false型とnull型を使用可能にする。

class Nil {
    public null $nil = null;
 
    public function foo(null $v): null { /* ... */ }
}

class Falsy {
    public false $nil = false;
 
    public function foo(false $v): false { /* ... */ }
}

Redundancy of ?null

?nullはコンパイルエラーになります。
これはPHPの現在の型宣言の解釈規則に沿ったものです。

Reflection

リフレクションはサポートされます。
ただし、null|TはReflectionNamedTypeになりますが、null|falseはReflectionUnionTypeになります。

以下はどのように見えるかの例です。

function dumpType(ReflectionUnionType $rt) {
    echo "Type $rt:\n";
    echo "Allows null: " . ($rt->allowsNull() ? "true" : "false") . "\n";
    foreach ($rt->getTypes() as $type) {
        echo "  Name: " . $type->getName() . "\n";
        echo "  String: " . (string) $type . "\n";
        echo "  Allows Null: " . ($type->allowsNull() ? "true" : "false") . "\n";
    }
}
 
function test1(): null|false { }
function test2(): ?false { }

dumpType((new ReflectionFunction('test1'))->getReturnType());
dumpType((new ReflectionFunction('test2'))->getReturnType());

/*
Type false|null:
Allows null: true
  Name: false
  String: false
  Allows Null: false
  Name: null
  String: null
  Allows Null: true

Type false|null:
Allows null: true
  Name: false
  String: false
  Allows Null: false
  Name: null
  String: null
  Allows Null: true
*/

Example

以下はユニットテストの例です。

class User {}
 
interface UserFinder
{
    function findUserByEmail(): User|null;
}
 
class AlwaysNullUserFinder implements UserFinder
{
    function findUserByEmail(): null
    {
        return null;
    }
}

現在、上記のコードはエラーになります。

Fatal error: Null can not be used as a standalone type

すなわち、findUserByEmail()はnullしか返さないにもかかわらず、User|nullという正しくない戻り値を設定しなければなりません。
ところが逆に静的アナライザはクラスを分析してfindUserByEmail()はUserクラスを返さないの警告を出してしまうため、さらなる混乱に見舞われます。

同様の問題は、falseのUNION型にも存在します。

Backward Incompatible Changes

後方互換性のない変更点はありません。

Proposed PHP Version

PHP8.2

Implementation

感想

gmp_random_seednull|falseを返すのですが、これまでこの型を正しく表現することはできませんでした。
本RFCによって、その問題が解消されます。
また、常にfalsenullを返す型も正確に表現可能になります。

この改善によって、PHPの型システムに存在していた僅かな穴がふさがれ、より表現力豊かになりました。

実はもうひとつだけ大きな穴が残っていまして、resource型と言うのですが、こいつは現行の型システムで表すことができません。
ただ、resouce型はどんどこ削除されてオブジェクトに置き換えられつつあるため、いずれは無くなると思います。
その時こそが、PHPの型システムの完成となるでしょう。

あとは(A&B)|Cみたいな型パズルの領域ですが、こんなのが役に立つとも思えないのですがどうでしょうかね。
交差型において進められていた実装も放棄されており、今後復活するかは微妙なところです。