rfc:noreturn_type

This is an old revision of the document!


PHP RFC: noreturn type

Introduction

There has been a trend over the past few years that concepts initially just expressed in PHP docblocks eventually become native PHP types.

Past examples are: scalar typehints, return types, union types, mixed types, and static types.

Our static analysis tools currently provide support for @return noreturn docblocks to denote functions that always throw or exit. Users of these tools have found that syntax useful to describe the behaviour of their own code, but we think it’s even more useful as a native return type, where PHP compile-time and runtime type-checks can guarantee its behaviour.

Proposal

Introduce a noreturn type that can be used in functions that always throw or exit.

Redirect functions that always call exit (either explicitly or implicitly) are good candidates for such a return type:

function redirect(string $uri): noreturn {
    header('Location: ' . $uri);
    exit();
}
 
function redirectToLoginPage(): noreturn {
    redirect('/login');
}

PHP developers can call these functions, safe in the knowledge that no statements after the function call will be evaluated:

function sayHello(?User $user) {
    if (!$user) {
        redirectToLoginPage();
    }
 
    echo 'Hello ' . $user->getName();
}

If, at some later date, the redirect function is changed so that it does sometimes return a value, a compile error is produced:

function redirect(string $uri): noreturn {
    if ($uri === '') {
        return; // Fatal error: A noreturn function must not return
    }
    header('Location: ' . $uri);
    exit();
}

If, instead, the above function is rewritten to have an implicit return, a TypeError is emitted:

function redirect(string $uri): noreturn {
    if ($uri !== '') {
        header('Location: ' . $uri);
        exit();
    }
}
 
redirect(''); // Uncaught TypeError: redirect(): Nothing was expected to be returned

Attempting to use yield inside a noreturn function produces a compile-time error:

function generateList(string $uri): noreturn {
    yield 1;
    exit();
}
// Fatal error: Generator return type must be a supertype of Generator

Applicability

Like void, the noreturn type is only valid when used as a function return type. Using noreturn as an argument or property type produces a compile-time error:

class A {
    public noreturn $x; // Fatal error
}

Variance

In type theory noreturn would be called a “bottom” type. That means it's effectively a subtype of every other type in PHP’s type system, including void.

It obeys the rules you might expect of a universal subtype:

Return type covariance is allowed:

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');
    }
}

Return type contravariance is prohibited:

abstract class Redirector
{
    abstract public function execute(): noreturn;
}
 
class BadRedirector extends Redirector
{
    public function execute(): void {} // Fatal error
}

Returning by reference with a noreturn type is allowed as well.

class A {
    public function &test(): int { ... }
}
class B extends A {
    public function &test(): noreturn { throw new Exception; }
}

Returning noreturn is also allowed in toString methods: <code php> class A implements Stringable { public function toString(): string { return “hello”; } } class B extends A { public function toString(): noreturn { throw new \Exception('not supported'); } } </code> === Allowed return types when a function always throws === Since noreturn is a subtype of all other types, a function that could be annotated with noreturn can still safely be annotated with another return type: <code php> function doFoo(): int { throw new \Exception(); } </code> ==== Prior art in other interpreted languages ==== * Hacklang has a noreturn type. * TypeScript has a never type that's also an explicit bottom type. * Python has a NoReturn type as part of its official typing library. ==== Prior art in PHP static analysis tools ==== In the absence of an explicit return type some PHP static analysis tools have also adopted support for noreturn or similar: * Psalm and PHPStan support the docblock return type /** @return noreturn */ * PHPStorm supports a custom PHP 8 attribute #[JetBrains\PhpStorm\NoReturn] ==== Comparison to void ==== Both noreturn and void are both only valid as return types, but there the similarity ends. When you call a function that returns void you generally expect PHP to execute the next statement after that function call. <code php> function sayHello(string $name): void { echo “Hello $name”; } sayHello('World'); echo “, it’s nice to meet you”; </code> But when you call a function that returns noreturn you explicitly do not expect PHP to execute whatever statement follows: <code php> function redirect(string $uri): noreturn { header('Location: ' . $uri); exit(); } redirect('/index.html'); echo “this will never be executed!”; </code> ==== Attributes vs types ==== Some might feel that noreturn belongs as a function/method attribute, potentially a root-namespaced one: Attribute form: <code php> #[\NoReturn] function redirectToLoginPage(): void {...} </code> Type form: <code php> function redirectToLoginPage(): noreturn {...} </code> We believe it’s more useful as a type. Internally PHP has a much more straightforward interpretation of return types than attributes, and PHP can quickly check variance rules for noreturn types just as it does for void. It's also tidier. ==== Naming ==== Naming is hard, but we believe noreturn is the best name for this type. Arguments for noreturn: * Very unlikely to be used as an existing class name. * Describes the behaviour of the function. Arguments for never: * It's a single word - noreturn does not have any visual separator between the two words and one cannot be sensibly added e.g. no-return. * It's a full-fledged type, rather than a keyword used in a specific situation. A far-in-the-future generics proposal could use never as a placeholder inside contravariant generic types. ===== Backwards Incompatible Changes ===== noreturn becomes a reserved word in PHP 8.1 ===== Proposed PHP Version(s) ===== 8.1 ===== Patches and Tests ===== Draft implementation here: https://github.com/php/php-src/pull/6761 ===== Proposed Voting Choices ===== Yes/no vote for adding noreturn (2/3 majority) Vote for noreturn vs never'' (simple majority)

rfc/noreturn_type.1617116137.txt.gz · Last modified: 2021/03/30 14:55 by mattbrown