46
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【PHP8.1】呼び出し元に返らない返り値の型が指定できるようになる

Posted at
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

noreturnvoidは、いずれも返り値の型にしか書けないということは同じですが、類似点はそれだけです。

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

Support noreturn types #6761

Vote

投票は2021/03/30から2021/04/13に行われ、賛成42反対11で受理されました。

またキーワードについて、noreturnneverどちらがよいかという投票も同時に行われました。
こちらはnoreturn14票・never34票となっており、neverに決定しました。

RFCの本文はnoreturn前提で書かれているのですが、まあそのうち書き替えられると思います。

感想

never型は、void型false疑似型同様、返り値にしか書けない特殊な型となります。

途中でexitするなんて他の言語ではなかなか考えづらいものがありますが、PHPの場合はリダイレクトという非常に自然な例が存在します。

function redirect(string $url): never {
	header('Location: ' . $url);
	exit;
}

これについては、他の書き方の方がかえって不自然になるでしょう。
こういうところに使えば、サジェストなどに出てくるため使い勝手がよりよくなりますね。

もっともフレームワークを使っていれば、あまり目にする機会はなさそうです。
たとえばLaravelだとユーザコード上でexitすることは考えられておらず、リダイレクトRedirectResponseを適当に設定してreturnで送り返せ、みたいな設計だったりするので、ユーザから見える範囲にnever型が現れてくることはないでしょう。

……いや待てddがあった!
これでdddumpの動作の違いをシグネチャで区別できるようになるぞ。
何の意味があるのかはわかりません。

46
17
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
46
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?