rfc:returntypehinting

This is an old revision of the document!


PHP RFC: Return Type Declarations

Introduction

Many developers would like to be able to declare the return type of a function. The basic idea of declaring a return type has been included in at least three RFCs and has been discussed in a few other places (see references). This RFC proposes a different approach from previous RFC's to accomplish this goal in a simple way.

Declaring return types has several motivators and use-cases:

  • Providing return types on interface implementations1).
  • Preventing sub-classes to break your return type
  • Preventing unintended return types
  • Documenting return type information that is not easily invalidated (unlike comments).

Proposal

This proposal adds an optional return type declaration to function declarations including closures, functions, generators, interface method declarations and class declarations. This RFC does not change the existing type declarations nor does it add new ones (see differences from past RFCs).

Example of function_declaration_stmt:

"function" ["&"] T_STRING "(" parameter_list ")" [":"  (T_STRING | array | callable)] "{"
    inner_statement_list
"}"

Here is a brief example of the syntax in action:

function foo(): array {
    return [];
}

More examples can be found in the Examples section.

Code which does not declare a return type continue to work exactly as it currently does. This RFC requires a return type to be declared only when a method inherits from a parent method that declares a return type; in all other cases it may be omitted.

Variance and Signature Validation

The enforcement of the declared return type during inheritance is covariant; this allows an over-riding method to declare a return type that is a sub-type of the original (illustrated in examples). This notably allows a class or interface to add a return type to an inherited method where one did not exist in the parent (also shown in examples).

If a mismatch is detected during compile time (e.g. a class improperly overriding a return type) then E_COMPILE_ERROR will be issued. If a type mismatch is detected when the function returns then E_RECOVERABLE_ERROR will be issued.

Covariant return types are considered to be type sound and are used in many other languages2).

Position of Type Declaration

The two major conventions in other programming languages for placing return type information are:

  • Before the function name
  • After the parameter list's closing parenthesis

The former position has been proposed in the past and the RFCs were either declined or withdrawn. One cited issue is that many developers wanted to preserve the ability to search for function foo to be able to find the definition for foo. A recent discussion about removing the function keyword has several comments that re-emphasized the value in preserving this.

The latter position is used in several languages3); notably C++11 also places the return type after the parameter lists for certain constructs such as lambdas and auto-deducing return types.

Declaring the return type after the parameter list had no shift/reduce conflicts in the parser.

Returning by Reference

This RFC does not change the location of & when returning by reference. The following examples are valid:

function &array_sort(array &$data) {
    return $data;
}
 
function &array_sort(array &$data): array {
    return $data;
}

Disallowing NULL on Return Types

Consider the following function:

function foo(): DateTime { 
    return null; // invalid
}

It declares that it will return DateTime but returns null; this type of situation is common in many languages including PHP. By design this RFC does not allow null to be returned in this situation for two reasons:

  1. This aligns with current parameter type behavior. When parameters have a type declared, a value of null is not allowed 4).
  2. Allowing null by default works against the purpose of type declarations. Type declarations make it easier to reason about the surrounding code. If null was allowed the programmer would always have to worry about the null case.

The Nullable Types RFC addresses this shortcoming and more.

Methods which cannot declare return types

Class constructors, destructors and clone methods may not declare return types. Their respective error messages are:

  • Fatal error: Constructor %s::%s() cannot declare a return type in %s on line %s
  • Fatal error: Destructor %s::__destruct() cannot declare a return type in %s on line %s
  • Fatal error: %s::__clone() cannot declare a return type in %s on line %s

Examples

Here are some snippets of both valid and invalid usage.

Examples of Valid Use

// Covariant return-type:
 
interface Collection {
    function map(callable $fn): Collection;
}
 
interface Set extends Collection {
    function map(callable $fn): Set;
}
// Overriding a method that did not have a return type:
interface Comment {}
interface CommentsIterator extends Iterator {
    function current(): Comment;
}
// Using a generator:
 
interface Collection extends IteratorAggregate {
    function getIterator(): Iterator;
}
 
class SomeCollection implements Collection {
    function getIterator(): Iterator {
        foreach ($this->data as $key => $value) {
            yield $key => $value;
        }
    }
}

Examples of Invalid Use

The error messages are taken from the current patch.


// Returned type does not match the type declaration
 
function get_config(): array {
    return 42;
}
get_config();

Catchable fatal error: The function get_config was expected to return an array and returned an integer in %s on line %d


// Int is not a valid type declaration
 
function answer(): int {
    return 42;
}
answer();

Catchable fatal error: The function answer was expected to return an object of class int and returned an integer in %s on line %d


// Cannot return null with a return type declaration
 
function foo(): DateTime {
    return null;
}
foo();

Catchable fatal error: The function foo was expected to return an object of class DateTime and returned null in %s on line %d


// Missing return type on override
 
class User {}
 
interface UserGateway {
    function find($id): User; 
}
 
class UserGateway_MySql implements UserGateway {
    // must return User or subtype of User
    function find($id) {
        return new User();
    }
}

Fatal error: Declaration of UserGateway_MySql::find should be compatible with UserGateway::find($id): User, return type missing in %s on line %d


// Generator return types can only be declared as Generator, Iterator or Traversable (compile time check)
 
function foo(): array {
    yield [];
}

Fatal error: Generators may only declare a return type of Generator, Iterator or Traversable, %s is not permitted in %s on line %d

Multiple Return Types

This proposal specifically does not allow declaring multiple return types; this is out of the scope of this RFC and would require a separate RFC if desired.

If you want to use multiple return types, simply omit a return type declaration and rely on PHP's excellent dynamic nature.

Reflection

A new ReflectionType class has been added (described below).

The following two methods have been added to ReflectionFunctionAbstract:

  1. hasReturnType() - returns bool
  2. getReturnType() - returns ReflectionType

Note that getReturnType will always return a ReflectionType object, even if there is no return type declared. In this case, ReflectionType::getTypeConstant() will return ReflectionType::TYPE_UNDECLARED.

ReflectionType

class ReflectionType implements Reflector {
    const TYPE_UNDECLARED = 0;
    const TYPE_ARRAY;    /* You may not rely on the value of TYPE_ARRAY */
    const TYPE_CALLABLE; /* You may not rely on the value of TYPE_CALLABLE */
    const TYPE_OBJECT;   /* You may not rely on the value of TYPE_OBJECT */
 
    public function getTypeConstant(); /* returns one of the ReflectionType::TYPE_* constants */
    public function getName(); /* returns the string version of the type */
    public function __toString(); /* returns the string version of the type */
}

This API was designed so you could use ReflectionType::getTypeConstant() in a switch statement to cover all cases, rather than be forced to use a nested if/else structure that calls isArray(), isCallable, etc like the ReflectionParameter API does. Compare these two examples:

// If ReflectionType mirrored ReflectionParameter's API
$ReflectionType = /* ... */;
 
if ($ReflectionType !== false) {
    if ($ReflectionType->isArray()) {
        // ...
    } elseif ($ReflectionType->isCallable()) {
        // ...
    } elseif ($ReflectionType->getClass() !== null) {
        // ...
    } else {
        // handle unknown type (future compatibility)
    }
} else {
    // ...
}
// The proposed API:
$ReflectionType = /* ... */;
 
switch ($ReflectionType->getTypeConstant()) {
    case ReflectionType::TYPE_ARRAY:
        // ...
        break;
    case ReflectionType::TYPE_CALLABLE:
        // ...
        break;
    case ReflectionType::TYPE_OBJECT:
        // ...
        break;
    case ReflectionType::TYPE_UNDECLARED:
        // ...
        break;
    default:
        // handle unknown type (future compatibility)
        break;
}

Additionally, if you have a ReflectionType API that matches ReflectionParameter, to see if there is no type provided you must do:

$ReflectionType = /* ... */;
 
if (!$ReflectionType->isArray() && !$ReflectionType->isCallable() && $ReflectionType->getClass() === null) {
    // no type provided
}

Compared to the proposed API:

$ReflectionType = /* ... */;
 
if ($ReflectionType->getTypeConstant() == ReflectionType::TYPE_UNDECLARED) {
    // no type provided
}

Note that this RFC does not alter ReflectionParameter in any way; for now ReflectionType is only used by return type information but was designed so that it could be reused for other type information in the future.

Differences from Past RFCs

This proposal differs from past RFCs in several key ways:

  • The return type is positioned after the parameter list. See Position of Type Declaration for more information about this decision.
  • We keep the current type options. Past proposals have suggested new types such as void, int, string or scalar; this RFC does not include any new types. Note that it does allow self and parent to be used as return types.
  • We keep the current search patterns. You can still search for function foo to find foo's definition; all previous RFCs broke this common workflow.
  • We allow return type declarations on all function types. Will Fitch's proposal suggested that we allow it for methods only.
  • We do not modify or add keywords. Past RFCs have proposed new keywords such as nullable and more. We still require the function keyword.

Other Impact

On Backward Compatiblity

This RFC is backwards compatible with previous PHP releases.

On SAPIs

There is no impact on any SAPI.

On Existing Extensions

The structs zend_function and zend_op_array have been changed; extensions that work directly with these structs may be impacted.

On Performance

An informal test indicates that performance has not seriously degraded. More formal performance testing can be done before voting phase.

Proposed PHP Version(s)

This RFC targets PHP 7.

Vote

This RFC modifies the PHP language syntax and therefore requires a two-third majority of votes.

Should return types as outlined in this RFC be added to the PHP language? Voting will end on November 14th, 2014.

Return Types
Real name Yes No
aeoris (aeoris)  
ajf (ajf)  
ben (ben)  
bwoebi (bwoebi)  
daverandom (daverandom)  
davey (davey)  
dm (dm)  
dmitry (dmitry)  
dragoonis (dragoonis)  
duodraco (duodraco)  
fa (fa)  
fredemmott (fredemmott)  
frozenfire (frozenfire)  
googleguy (googleguy)  
gooh (gooh)  
guilhermeblanco (guilhermeblanco)  
hywan (hywan)  
ivonascimento (ivonascimento)  
jedibc (jedibc)  
jpauli (jpauli)  
jwage (jwage)  
kalle (kalle)  
kinncj (kinncj)  
klaussilveira (klaussilveira)  
krakjoe (krakjoe)  
kriscraig (kriscraig)  
leigh (leigh)  
levim (levim)  
lstrojny (lstrojny)  
malukenho (malukenho)  
mbeccati (mbeccati)  
mikemike (mikemike)  
mrook (mrook)  
nikic (nikic)  
patrickallaert (patrickallaert)  
philstu (philstu)  
pollita (pollita)  
ralphschindler (ralphschindler)  
rdlowrey (rdlowrey)  
rdohms (rdohms)  
reeze (reeze)  
remi (remi)  
rogeriopradoj (rogeriopradoj)  
salathe (salathe)  
sebastian (sebastian)  
seld (seld)  
stas (stas)  
thekid (thekid)  
weierophinney (weierophinney)  
willfitch (willfitch)  
yunosh (yunosh)  
zeev (zeev)  
Count: 49 3

Patches and Tests

Levi Morrison has provided a pull request that needs some work before being merged, but is a fully-working implementation with tests.

Joe Watkins provided an implementation based on the PHP 5.x series which includes tests, reflection and opcache support; it's pull request is here: https://github.com/php/php-src/pull/653

Future Work

Ideas for future work which are out of the scope of this RFC include:

  • Allowing functions to declare that they do not return anything at all (void)
  • Allowing nullable types(such as ?DateTime). This is discussed in a related RFC: Declaring Nullable Types
  • Unifying the reflection type API by having ReflectionParameter use ReflectionType.
  • Improving parameter variance. Currently parameter types are invariant while they could be contravariant.
  • Improving runtime performance by doing type analysis.
  • Updating documentation to use the new return type syntax.

References

In the meeting in Paris on November 2005 it was decided that PHP should have return type declarations and some suggestions were made for syntax. Suggestion 5 is nearly compatible with this RFC; however, it requires the addition of a new token T_RETURNS. This RFC opted for a syntax that does not require additional tokens so returns was replaced by a colon.

The following (tiny) patch would allow the syntax in suggestion 5 to be used alongside the current syntax. This RFC does not propose that both versions of syntax should be used; the patch just shows how similar this RFC is to that suggestion from 2005.

https://gist.github.com/krakjoe/f54f6ba37e3eeab5f705

Changelog

  • v1.1: RFC now targets PHP 7.
  • v1.2: disallow return types for constructors, destructors and clone methods.
  • v1.3: reworked Reflection support to use new ReflectionType class
  • v1.3.1: renamed ReflectionType::IS_* constants to TYPE_*, renamed ->getKind() to ->getTypeConstant()
1)
See Variance and Signature Validation and examples for more details on how this works
2)
C++, Java and others use covariant return types, just as is proposed here.
3)
Hack, Haskell, Go, Erlang, ActionScript, TypeScript and more all put the return type after the parameter list
4)
Except when the parameter has a null default
rfc/returntypehinting.1415067477.txt.gz · Last modified: 2017/09/22 13:28 (external edit)