rfc:never_for_parameter_types

PHP RFC: Never For Parameter Types

Introduction

Parameters in PHP are contravariant to preserve Liskov substitution. This means that if class B extends class A, then redefines a function call, the entire type of that parameter from class A must be present in the type of the parameter in class B:

<?php
 
abstract class A {
 
  abstract public function foo(int $arg);
 
}
 
class B extends A {
 
  public function foo(int|float $arg) {
    return round($arg) + 1;
  }
 
}

Thus, the more specific a type is for an parameter in a base class, the more broad it can be in an extending class with the requirement that it must also include the type from the base class.

Since never is a bottom type within the PHP engine, all other types contain it. This RFC proposes allowing never as a valid parameter type for functions.

Use Cases

Interfaces and Abstracts

With the never type available, interfaces and abstracts would be able to allow implementing classes to provide a type without specifying any details about what that type must be. This would allow use cases such as the following:

<?php
 
interface CollectionInterface {
 
  public function add(never $input): self;
 
}

Implementers of the CollectionInterface could then specify any type they want. Functions which use collections could then type against the interface, allowing any of the variously typed implementations to be provided, though additional docblocks would be required for static analysis as this feature does not actually solve the generics issue.

In this way, allowing the never type for interfaces and abstracts could be a method of providing minimal stop-gap support for generics type behavior (covariant polymorphism) while avoiding the challenges that providing generics represents. It would also not prevent or make it more difficult to provide full generics in the future.

Using never as a minimal form of generics in this way would require additional work, such as docblock information, for the static analysis to treat them as such, since this features is not generics.

Internal Classes and Interfaces

Providing internal interfaces that require a type but do not specify which type can be very beneficial. This has been encountered recently with attempts to update ArrayAccess.

interface ArrayAccess {
 
  public function offsetExists(never $offset): bool;
 
  public function offsetGet(never $offset): mixed;
 
  public function offsetSet(never $offset, never $value): void;
 
  public function offsetUnset(never $offset): void;
 
}

Currently, internally provided interfaces that must provide flexible parameters for implementers specify the parameter as mixed, however as mixed is the top type in PHP it cannot be narrowed at all, and any code which implements the interface can not fully utilize the type system in place in PHP.

Never as a parameter type allows us to provide interfaces in core that are correctly typed according to the type hierarchy and still follow LSP.

Design Considerations

Never vs A New Type

While it could be argued that the intended meaning of never was explicitly that code using this type would terminate, a new type offers several issues over using never:

  • never already represents the concept of a bottom type in PHP due to its usage with return types. The engine already has the concept of this type built in, but is not currently exposing it for use with arguments. Having multiple bottom types not only doesn't make sense, but an example of another language doing this cannot be found.
  • never correctly indicates that the code which uses it for an argument type can never be called directly.
  • This usage has precedence in other languages; see below.

Other Languages

There are several languages which contain a bottom type, some of which even use never as their bottom type. The behavior described in this RFC is in fact how never behaves and can be used in TypeScript, which also uses never as its bottom type.

Scala also uses the bottom type to denote covariant parameter polymorphism, though the bottom type in Scala is Nothing.

Proposal

Allow the use of never as a type for arguments in interfaces and classes. This would have the following semantics:

  • never cannot be used in an intersection type or union type. Any intersection would reduce to never, and any union would reduce never out of the union, as never is the identity type of unions.
  • Attempting to call code directly that uses the never type for an argument would result in a TypeError, as no zval will match this type.

This means that an interface or class could allow implementers and subclasses to declare a type for an argument without restricting that type to anything particular.

Backward Incompatible Changes

None

Proposed PHP Version(s)

This change is proposed for PHP 8.2

RFC Impact

To SAPIs

None

To Existing Extensions

None

To Opcache

None

New Constants

None

php.ini Defaults

None

Unaffected PHP Functionality

Existing PHP typing will be unaffected. As never is the bottom type in the PHP type hierarchy, all types contain it, and thus no existing typing will need to be changed or updated.

Future Scope

To fully function as a bottom type in PHP, never could be allowed in other places such as class properties. This is left for future RFCs.

Proposed Voting Choices

Allow never as a parameter type as described: yes/no. A 2/3 vote is required to pass.

Patches and Tests

Implementation

After the project is implemented, this section should contain

  1. the version(s) it was merged into
  2. a link to the git commit(s)
  3. a link to the PHP manual entry for the feature
  4. a link to the language specification section (if any)

References

Changelog

  • 0.1: Initial Proposal
  • 0.2: Removed explicit widening
rfc/never_for_parameter_types.txt · Last modified: 2021/08/24 11:53 by jordanrl