function foo():XXX{
exit;
}
この関数の返り値の型は何にすればいいでしょうか。
null?
void?
nullはnullという型ですし、voidは『値を返さない』であって『呼び出し元に返らない』ではありません。
ということで『呼び出し元に返らない』を明記できる型が提案されました。
返らないのに返り値とは。
PHP8.1以降ではこう書けるようになります。
function foo():never{
exit;
}
以下は該当のRFC、PHP RFC: noreturn typeの日本語訳です。
PHP RFC: noreturn type
Introduction
ここ数年の傾向として、元々はPHP docで表現されていた型がPHPネイティブになっていくということがあります。
過去の例としてはスカラー型、返り値の型、UNION型、mixed型、static型などです。
現在、PHPの静的解析ツールは、常に例外を発したり常にexit
したりする関数を示すために、@return noreturn
構文をサポートしています。
ツールのユーザは、自分のコードの動作を表すためにこの構文が便利だと感じていると思いますが、PHPネイティブのコンパイル・実行時の型チェックがサポートすればより便利になると思います。
Proposal
戻り値の型としてnoreturn
を導入する。
リダイレクトする関数は、この戻り値型のよいサンプルです。
function redirect(string $uri): noreturn {
header('Location: ' . $uri);
exit();
}
function redirectToLoginPage(): noreturn {
redirect('/login');
}
PHP開発者は、この関数を呼び出したときに、その後の文が評価されないことが保証されるという安心感を得ることができます。
function sayHello(?User $user) {
if (!$user) {
redirectToLoginPage();
}
echo 'Hello ' . $user->getName(); // redirectToLoginPageが呼ばれたらここには絶対に来ない
}
後からredirect
関数にreturn
を追加しようとしても、コンパイルエラーが発生します。
function redirect(string $uri): noreturn {
if ($uri === '') {
return; // Fatal error: A noreturn function must not return
}
header('Location: ' . $uri);
exit();
}
暗黙的なreturn
にしようとした場合は、TypeErrorが発生します。
function redirect(string $uri): noreturn {
if ($uri !== '') {
header('Location: ' . $uri);
exit();
}
}
redirect(''); // Uncaught TypeError: redirect(): Nothing was expected to be returned
noreturn
関数内でyield
を使うとコンパイルエラーになります。
function generateList(string $uri): noreturn {
yield 1;
exit();
}
// Fatal error: Generator return type must be a supertype of Generator
Applicability
noreturn
型は、void
型と同じく返り値の型としてのみ有効です。
引数やプロパティとして使おうとするとコンパイルエラーになります。
class A {
public noreturn $x; // Fatal error
}
Variance
型理論において、noreturn
型はボトム型とされます。
すなわち、PHPの型システムにおいては、noreturn
型はvoid
を含む全ての型のサブタイプになるということです。
従って、noreturn
型は他のサブタイプと同じルールに従います。
返り値の型を狭めることができます。
abstract class Person
{
abstract public function hasAgreedToTerms(): bool;
}
class Kid extends Person
{
public function hasAgreedToTerms(): noreturn
{
throw new \Exception('Kids cannot legally agree to terms');
}
}
返り値の型を広げることはできません。
abstract class Redirector
{
abstract public function execute(): noreturn;
}
class BadRedirector extends Redirector
{
public function execute(): void {} // Fatal error
}
リファレンスを使うことも可能です。
class A {
public function &test(): int { ... }
}
class B extends A {
public function &test(): noreturn { throw new Exception; }
}
__toString
メソッドにも適用可能です。
class A implements Stringable {
public function __toString(): string {
return "hello";
}
}
class B extends A {
public function __toString(): noreturn {
throw new \Exception('not supported');
}
}
noreturn
型は全ての型のサブタイプであるため、他の型で問題なくアノテーションできます。
function doFoo(): int
{
throw new \Exception();
}
Prior art in other interpreted languages
他のインタプリタ型での同様な実装。
Prior art in PHP static analysis tools
PHP静的解析ツールでの同様な実装。
- PsalmとPHPStanは、
/** @return noreturn */
をサポートしている。 - PHPStormは、PHP8のアトリビュートで
#[JetBrains\PhpStorm\NoReturn]
をサポートしている。
Comparison to void
noreturn
とvoid
は、いずれも返り値の型にしか書けないということは同じですが、類似点はそれだけです。
void
型の関数を呼んだ場合、通常はそれ以降のプログラムも実行されるという想定です。
function sayHello(string $name): void {
echo "Hello $name";
}
sayHello('World');
echo ", it’s nice to meet you";
noreturn
型の関数は、その後の文が実行されることはありません。
function redirect(string $uri): noreturn {
header('Location: ' . $uri);
exit();
}
redirect('/index.html');
echo "this will never be executed!";
Naming
ネーミングは難しい。
- noreturn
・既存のクラス名として使われている可能性は低い。
・関数っぽい名前。
- never
・1単語であり、no-return
みたいに区切りを入れたくなる衝動がおこらない。
・特定状況で使われるキーワードではなく、本格的な型として扱える。遠い将来ジェネリクスが入ったときにも使える単語だろう。
Backwards Incompatible Changes
互換性のない変更として、never
が予約語に追加されます。
Proposed PHP Version(s)
PHP 8.1。
Patches and Tests
Vote
投票は2021/03/30から2021/04/13に行われ、賛成42反対11で受理されました。
またキーワードについて、noreturn
・never
どちらがよいかという投票も同時に行われました。
こちらはnoreturn
14票・never
34票となっており、never
に決定しました。
RFCの本文はnoreturn
前提で書かれているのですが、まあそのうち書き替えられると思います。
感想
never型は、void型やfalse疑似型同様、返り値にしか書けない特殊な型となります。
途中でexitするなんて他の言語ではなかなか考えづらいものがありますが、PHPの場合はリダイレクトという非常に自然な例が存在します。
function redirect(string $url): never {
header('Location: ' . $url);
exit;
}
これについては、他の書き方の方がかえって不自然になるでしょう。
こういうところに使えば、サジェストなどに出てくるため使い勝手がよりよくなりますね。
もっともフレームワークを使っていれば、あまり目にする機会はなさそうです。
たとえばLaravelだとユーザコード上でexit
することは考えられておらず、リダイレクトもRedirectResponse
を適当に設定してreturn
で送り返せ、みたいな設計だったりするので、ユーザから見える範囲にnever型が現れてくることはないでしょう。
……いや待てddがあった!
これでdd
とdump
の動作の違いをシグネチャで区別できるようになるぞ。
何の意味があるのかはわかりません。