rfc:interface-default-methods

PHP RFC: Interface Default Methods

Introduction

Over time, authors sometimes want to add methods to an interface. Today, this causes large breakages to every implementor. This RFC proposes a way to reduce the scale of breakage.

Additionally, sometimes interfaces can implement some functionality of their interface in terms of other parts of their interface. For example, the interface Countable could implement function isEmpty(): bool by using $this->count() == 0. Today, traits are often used to accomplish this, but a default interface implementation could be used instead, and would be simpler. When interfaces are designed with this technique in mind, it can enable productivity of the interface over time.

For example, see Rust's Iterator trait which has more than 40 functions with default implementations and the implementor mostly needs to implement a single function, next(): Option<T>. These functions with default behaviors have been stabilized over time with little to no breakage in practice. For example, for_each was added in v1.21 and map_while was added in v1.57.

Proposal

Interface methods can now provide a method body. This implementation will be used if the implementer does not otherwise provide an implementation. Default implementations are available for both instance and static methods.

interface Example {
  // Today, you cannot specify an implementation, only a
  // signature.
  function method1(): void;
 
  // With this RFC, you can provide an implementation:
  function method2(): void {
    echo __METHOD__, "\n";
  } 
}

Interface methods can also now be private if they a have method body. The purpose here is to allow helper routines if the default implementations of the public methods need them.

The methods with default implementations are inherited similarly to abstract base classes, as opposed to how traits behave. For instance, a private method on the interface is not accessible to the class which implements the interface; with a trait it would be.

Default Method Resolution

Default methods for interfaces introduce a form of multiple inheritance. How is a method selected?

Java's resolution algorithm looks like this:

  1. Class definitions always win. If a class, or a parent class, defines a concrete method, then this will always win over all possible default methods.
  2. More specific interfaces take priority over less specific ones. If interfaces A and B exist and B extends A, and both have a default method for the same method, then when a class implements B directly or indirectly the method from B will take precedence over the method from A.
  3. If the above rules fail, then the class needs to implement the method itself, possibly delegating to one of the interfaces.

This RFC proposes the same. However, rule 2 is only partially implemented at this time.

Here's an example of a class delegating to another method:

interface Interface1 {
    function method1() { echo __METHOD__ . "\n"; }
}
 
interface Interface2 {
    function method1() { echo __METHOD__ . "\n"; }
}
 
class Class1 implements Interface1, Interface2 {
    function method1() {
        Interface1::method1();
    }
}
 
(new Class1())->method1(); // Interface1::method1

Parent Scoping

In interface default methods, you cannot use the parent:: scoping mechanism. The following does not work:

interface Interface1 {
    function method1() { echo __METHOD__, PHP_EOL; }
}
 
interface Interface2 extends Interface1 {
    function method1() { parent::method1(); }
}

The reason is that interfaces can extend multiple other interfaces e.g.

interface Interface3 extends Interface1, Interface2 {}

The name of the interface should be used instead:

interface Interface2 extends Interface1 {
    function method1() { Interface1::method1(); }
}

However, if a sub-class of the inheriting class calls parent::method1(), it will work:

interface Interface1 {
    function method1() { echo __METHOD__, PHP_EOL; }
}
 
class Class1 implements Interface1 {
    // Inherits Interface1::method1() here.
}
 
class Class2 extends Class1 {
    function method1() { parent::method1(); }
}
 
(new Class2())->method1);
// output:
// Interface1::method1

Cancelling Default Methods

If an interface method extends a parent interface method which has a default, this prevents using the default method for classes which implement the child interface but do not directly implement the parent one:

interface Interface1 {
    function method1() { echo __METHOD__, "\n"; }
}
 
interface Interface2 extends Interface1 {
    function method1();
}
 
/* Would be an error because method1 has not been implemented.
class Class1 implements Interface2 {
    // error: method1 has not been implemented.
}
 */
 
// This is subtly different, but valid:
class Class1 implements Interface1, Interface2 {}

The behavior could go either way. I picked this behavior because if it's wrong, it's easier to correct than the other way around.

Backward Incompatible Changes

None, as long as you do not use the feature.

If you do use the feature:

  • Adding a default implementation to an existing interface method will not break existing code, because every existing usage has a higher priority than the default.
  • If you add a new method to an interface, there is a compatibility break. The impact of the break is limited to places where the implementor/inheritor of the interface has a method of the same name.

Proposed PHP Version(s)

PHP 8.3.

RFC Impact

To Extensions

Modules can specify an interface implementation as well. These internal default methods should not be marked with ZEND_ACC_ABSTRACT and should be instead marked with ZEND_ACC_DEFAULT_METHOD.

To Opcache

Opcache should also work with this feature. The proof of concept implementation has not triggered any issues so far in CI.

To the Ecosystem

Previously, interface methods were not allowed to have method bodies. These tools such as parsers, code analyzers, etc will need to be updated.

Open Issues

Make sure there are no open issues when the vote starts!

Future Scope

This feature may be used to enhance existing interfaces in PHP.

  • Countable could add function isEmpty(): bool { return $this->count() == 0; }.
  • Iterator could add methods like map, filter, and reduce which behave similarly to array_map, array_filter, and array_reduce.

Voting

The vote will be a simple yes/no vote on whether to include the feature.

Interface Default Methods
Real name Yes No
alcaeus (alcaeus)  
alec (alec)  
ashnazg (ashnazg)  
brzuchal (brzuchal)  
bwoebi (bwoebi)  
crell (crell)  
derick (derick)  
dragoonis (dragoonis)  
ericmann (ericmann)  
galvao (galvao)  
heiglandreas (heiglandreas)  
imsop (imsop)  
kalle (kalle)  
levim (levim)  
mauricio (mauricio)  
nicolasgrekas (nicolasgrekas)  
ocramius (ocramius)  
pierrick (pierrick)  
pmmaga (pmmaga)  
pollita (pollita)  
ramsey (ramsey)  
rasmus (rasmus)  
sebastian (sebastian)  
sergey (sergey)  
sirsnyder (sirsnyder)  
stas (stas)  
svpernova09 (svpernova09)  
thekid (thekid)  
theseer (theseer)  
timwolla (timwolla)  
trowski (trowski)  
weierophinney (weierophinney)  
Final result: 15 17
This poll has been closed.

Patches and Tests

Here is a work-in-progress pull request: https://github.com/php/php-src/pull/11467.

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

Links to external references, discussions or RFCs

Rejected Features

Keep this updated with features that were discussed on the mail lists.

rfc/interface-default-methods.txt · Last modified: 2023/07/17 15:06 by levim