Good morning internals
I’d like to test the waters about an RFC idea: allowing traits to implement
interfaces, and consequently a class that uses such a trait will
automatically implement the interface as well.
The original idea comes from Rust, where traits can be used as types. I
read a very inspiring post suggested by Larry, on the topic of “classic
inheritance” vs the way Rust and Go approach it [1]. The tl;dr is that both
Rust and Go solve several pitfalls of classical inheritance (the diamond
problem and inheritance abuse), thanks to a much simpler approach. In
Rust’s case that is by using traits. If you have the time, I highly
recommend reading that post, it’s super interesting and it gives a lot of
good arguments for rethinking inheritance.
Back to PHP, using traits as types seems impossible, since traits are a
compile-time copy/paste mechanism, which means there’s no type information
available about them at runtime.
However, allowing traits to implement interfaces would solve this problem:
these interfaces would be copied over to classes during compile-time, and
the interface’s type information is available at runtime. On top of that,
traits already have well-defined rules for conflict resolution, so we
wouldn’t need any additional syntax to handle edge cases.
Even though PHP traits differ from Rust, PHP developers already seem to
like the idea of being able to “attach a type to a trait” one way or
another. Let me name a couple of things that happen today:
Laravel often provides “default implementations” for their interfaces
via a trait [2]. As mentioned before, traits already deal with
conflict-resolution, so method collisions aren’t a blocker.
Both PHPStan and Psalm have an annotation that forces trait users to
implement an interface [3], which is essentially the feature I’m
describing, albeit via docblock annotations instead of proper syntax.
Even though it was not accepted, the interface default methods RFC
approached the problem from a different angle [4]. While a majority
disagreed that interfaces should implement their own methods directly, I
remember it was a heavily debated topic, and believe that approaching it
from the other side might be easier to accept.
In the end, the goal of this RFC would be to promote a “new way” of
inheritance, which is described in depth in that post I mentioned earlier
[1]. It’s a different programming style, but I think there are good
arguments to be made for it, even though it will likely not be everyone’s
cup of tea. Modern languages like Rust and Go show that there’s merit in
rethinking classical inheritance, and there are signs that the PHP
community is open to it as well, given the examples with Laravel’s default
implementation traits, static analyser support, as well as the interface
default methods RFC last year.
If internals are open to the idea, I would like to draft a proper RFC for
it. Please let me know your thoughts!
Brent
Links:
[1] https://lwn.net/Articles/548560/
[2a]
https://github.com/laravel/framework/blob/11.x/src/Illuminate/Auth/Authenticatable.php
[2b]
https://github.com/laravel/framework/blob/11.x/src/Illuminate/Contracts/Auth/Authenticatable.php
[2c]
https://github.com/laravel/framework/blob/11.x/src/Illuminate/Contracts/Auth/MustVerifyEmail.php
[2d]
https://github.com/laravel/framework/blob/11.x/src/Illuminate/Auth/MustVerifyEmail.php
[3a]
https://phpstan.org/writing-php-code/phpdocs-basics#enforcing-implementing-an-interface-for-traits
[3b]
https://psalm.dev/docs/annotating_code/supported_annotations/#psalm-require-implements
On Tue, 27 August in 2024 at 10:31, Brent Roose brent.roose@jetbrains.com
wrote:
Good morning internals
I’d like to test the waters about an RFC idea: allowing traits to
implement interfaces, and consequently a class that uses such a trait will
automatically implement the interface as well.The original idea comes from Rust, where traits can be used as types. I
read a very inspiring post suggested by Larry, on the topic of “classic
inheritance” vs the way Rust and Go approach it [1]. The tl;dr is that both
Rust and Go solve several pitfalls of classical inheritance (the diamond
problem and inheritance abuse), thanks to a much simpler approach. In
Rust’s case that is by using traits. If you have the time, I highly
recommend reading that post, it’s super interesting and it gives a lot of
good arguments for rethinking inheritance.Back to PHP, using traits as types seems impossible, since traits are a
compile-time copy/paste mechanism, which means there’s no type information
available about them at runtime.However, allowing traits to implement interfaces would solve this problem:
these interfaces would be copied over to classes during compile-time, and
the interface’s type information is available at runtime. On top of that,
traits already have well-defined rules for conflict resolution, so we
wouldn’t need any additional syntax to handle edge cases.Even though PHP traits differ from Rust, PHP developers already seem to
like the idea of being able to “attach a type to a trait” one way or
another. Let me name a couple of things that happen today:
Laravel often provides “default implementations” for their interfaces
via a trait [2]. As mentioned before, traits already deal with
conflict-resolution, so method collisions aren’t a blocker.Both PHPStan and Psalm have an annotation that forces trait users to
implement an interface [3], which is essentially the feature I’m
describing, albeit via docblock annotations instead of proper syntax.Even though it was not accepted, the interface default methods RFC
approached the problem from a different angle [4]. While a majority
disagreed that interfaces should implement their own methods directly, I
remember it was a heavily debated topic, and believe that approaching it
from the other side might be easier to accept.In the end, the goal of this RFC would be to promote a “new way” of
inheritance, which is described in depth in that post I mentioned earlier
[1]. It’s a different programming style, but I think there are good
arguments to be made for it, even though it will likely not be everyone’s
cup of tea. Modern languages like Rust and Go show that there’s merit in
rethinking classical inheritance, and there are signs that the PHP
community is open to it as well, given the examples with Laravel’s default
implementation traits, static analyser support, as well as the interface
default methods RFC last year.If internals are open to the idea, I would like to draft a proper RFC for
it. Please let me know your thoughts!Brent
Links:
[1] https://lwn.net/Articles/548560/
[2a]
https://github.com/laravel/framework/blob/11.x/src/Illuminate/Auth/Authenticatable.php[2b]
https://github.com/laravel/framework/blob/11.x/src/Illuminate/Contracts/Auth/Authenticatable.php
[2c]
https://github.com/laravel/framework/blob/11.x/src/Illuminate/Contracts/Auth/MustVerifyEmail.php[2d]
https://github.com/laravel/framework/blob/11.x/src/Illuminate/Auth/MustVerifyEmail.php[3a]
https://phpstan.org/writing-php-code/phpdocs-basics#enforcing-implementing-an-interface-for-traits[3b]
https://psalm.dev/docs/annotating_code/supported_annotations/#psalm-require-implements
Hi, Brent!
Let me link another relevant discussion about adding
@(phpstan|psalm)-require-implements
to the core:
https://externals.io/message/116802 .
--
Valentin
Hi Brent,
wt., 27 sie 2024 o 09:28 Brent Roose brent.roose@jetbrains.com napisał(a):
Good morning internals
I’d like to test the waters about an RFC idea: allowing traits to
implement interfaces, and consequently a class that uses such a trait will
automatically implement the interface as well.The original idea comes from Rust, where traits can be used as types. I
read a very inspiring post suggested by Larry, on the topic of “classic
inheritance” vs the way Rust and Go approach it [1]. The tl;dr is that both
Rust and Go solve several pitfalls of classical inheritance (the diamond
problem and inheritance abuse), thanks to a much simpler approach. In
Rust’s case that is by using traits. If you have the time, I highly
recommend reading that post, it’s super interesting and it gives a lot of
good arguments for rethinking inheritance.Back to PHP, using traits as types seems impossible, since traits are a
compile-time copy/paste mechanism, which means there’s no type information
available about them at runtime.However, allowing traits to implement interfaces would solve this problem:
these interfaces would be copied over to classes during compile-time, and
the interface’s type information is available at runtime. On top of that,
traits already have well-defined rules for conflict resolution, so we
wouldn’t need any additional syntax to handle edge cases.Even though PHP traits differ from Rust, PHP developers already seem to
like the idea of being able to “attach a type to a trait” one way or
another. Let me name a couple of things that happen today:
Laravel often provides “default implementations” for their interfaces
via a trait [2]. As mentioned before, traits already deal with
conflict-resolution, so method collisions aren’t a blocker.Both PHPStan and Psalm have an annotation that forces trait users to
implement an interface [3], which is essentially the feature I’m
describing, albeit via docblock annotations instead of proper syntax.Even though it was not accepted, the interface default methods RFC
approached the problem from a different angle [4]. While a majority
disagreed that interfaces should implement their own methods directly, I
remember it was a heavily debated topic, and believe that approaching it
from the other side might be easier to accept.
With the recent RFC proposal for Default Expressions
https://wiki.php.net/rfc/default_expression [5], I believe it presents an
excellent opportunity to revisit the Interface Default Methods proposal.
The Default Expressions RFC addresses similar functionality and, when
combined with an opt-in feature flag, could resolve many concerns raised
during the previous discussion.
Opt-In Feature Flag: To address backward compatibility concerns, I
propose the introduction of a feature flag, such as declare(default_methods
= 1);, that could be applied when implementing an interface or when an
interface extends another. This approach would allow developers to opt-in
explicitly, preventing unintended BC breaks and ensuring that the feature
is adopted carefully and intentionally.
Backward Compatibility Concerns: The main concern from the previous
discussion was the risk of BC breaks when new methods are added to an
interface, potentially conflicting with existing implementations. Although
the original RFC suggested that default implementations could mitigate
these risks, contributors were worried that this might encourage developers
to introduce BC breaks without proper versioning. The opt-in flag would
make it clear when the feature is being used, thereby reducing the risk of
unintentional conflicts.
Complexity and Developer Experience: While the feature could
significantly improve the developer experience, it also introduces
complexity in how interfaces are used. To alleviate this, the default
keyword could be explicitly used to mark default methods, making it easier
for developers to understand and manage. For example:
interface I1 {
default public function foo(): int {
return \PHP_INT_MAX;
}
}
This explicit marking not only clarifies the intention behind the method
but also aids in distinguishing between regular and default methods,
simplifying the mental model required to work with interfaces.
I believe these adjustments could make the Interface Default Methods more
palatable to the community, ensuring that the feature enhances PHP without
introducing unnecessary risks.
Just thinking out loud here - looking forward to hearing some thoughts.
Cheers,
Michał Marcin Brzuchalski
Links:
[4] Interface Default Methods RFC
https://wiki.php.net/rfc/interface-default-methods
[5] Default Expressions RFC <https://wiki.php.net/rfc/default_expression
Hi Brent,
wt., 27 sie 2024 o 09:28 Brent Roose brent.roose@jetbrains.com napisał(a):
Good morning internals
I’d like to test the waters about an RFC idea: allowing traits to implement interfaces, and consequently a class that uses such a trait will automatically implement the interface as well.
The original idea comes from Rust, where traits can be used as types. I read a very inspiring post suggested by Larry, on the topic of “classic inheritance” vs the way Rust and Go approach it [1]. The tl;dr is that both Rust and Go solve several pitfalls of classical inheritance (the diamond problem and inheritance abuse), thanks to a much simpler approach. In Rust’s case that is by using traits. If you have the time, I highly recommend reading that post, it’s super interesting and it gives a lot of good arguments for rethinking inheritance.
Back to PHP, using traits as types seems impossible, since traits are a compile-time copy/paste mechanism, which means there’s no type information available about them at runtime.
However, allowing traits to implement interfaces would solve this problem: these interfaces would be copied over to classes during compile-time, and the interface’s type information is available at runtime. On top of that, traits already have well-defined rules for conflict resolution, so we wouldn’t need any additional syntax to handle edge cases.
Even though PHP traits differ from Rust, PHP developers already seem to like the idea of being able to “attach a type to a trait” one way or another. Let me name a couple of things that happen today:
• Laravel often provides “default implementations” for their interfaces via a trait [2]. As mentioned before, traits already deal with conflict-resolution, so method collisions aren’t a blocker.
• Both PHPStan and Psalm have an annotation that forces trait users to implement an interface [3], which is essentially the feature I’m describing, albeit via docblock annotations instead of proper syntax.
• Even though it was not accepted, the interface default methods RFC approached the problem from a different angle [4]. While a majority disagreed that interfaces should implement their own methods directly, I remember it was a heavily debated topic, and believe that approaching it from the other side might be easier to accept.
With the recent RFC proposal for Default Expressions
https://wiki.php.net/rfc/default_expression [5], I believe it
presents an excellent opportunity to revisit the Interface Default
Methods proposal. The Default Expressions RFC addresses similar
functionality and, when combined with an opt-in feature flag, could
resolve many concerns raised during the previous discussion.
I... have no idea why you're conflating the Interface Default Methods RFC and the Default Expressions RFC. They have nothing to do with each other beyond having the word "default" in their name.
I would be very much in favor of revisiting Interface Default Methods, though, as I think it would be a strong feature, and it's one found in nearly all of our sibling languages at this point. PHP is weird for not having them. I believe that would also be a better approach than traits that link to interfaces, which achieves not-quite the same result with more steps.
Opt-In Feature Flag: To address backward compatibility concerns, I
propose the introduction of a feature flag, such as
declare(default_methods = 1);
, that could be applied when
implementing an interface or when an interface extends another. This
approach would allow developers to opt-in explicitly, preventing
unintended BC breaks and ensuring that the feature is adopted carefully
and intentionally.
Dear god no, no more feature flags. Remember that those affect a file, not a class, which are not necessarily 1:1.
Besides, there's an opt-in way to skip the defaults already: Implement your own version. The argument that "an interface author might introduce a new method with a default that is not quite perfect for every implementing class" never carried much weight with me, as it's a fringe edge case, unlikely to happen, and easily resolvable when it does. (Via the exact same means as adding a new method without a default implementation.)
Complexity and Developer Experience: While the feature could
significantly improve the developer experience, it also introduces
complexity in how interfaces are used. To alleviate this, thedefault
keyword could be explicitly used to mark default methods, making it
easier for developers to understand and manage. For example:interface I1 {
default public function foo(): int { return \PHP_INT_MAX; } }
This explicit marking not only clarifies the intention behind the
method but also aids in distinguishing between regular and default
methods, simplifying the mental model required to work with interfaces.
How? All I see here is another keyword I have to use. It doesn't do anything extra for me as a consumer of that interface. It doesn't add disambiguation to the language, either for the engine or human readers (as the presence of the body does that already). It just gives me 8 more characters to type.
Just thinking out loud here - looking forward to hearing some thoughts.
My thought is we should just pass Default Interface Methods and be done with it. :-)
--Larry Garfield
I would be very much in favor of revisiting Interface Default Methods, though, as I think it would be a strong feature, and it's one found in nearly all of our sibling languages at this point. PHP is weird for not having them. I believe that would also be a better approach than traits that link to interfaces, which achieves not-quite the same result with more steps.
I too am all for revisiting Interface Default Methods.
Lack of interface default methods is one of the most painful missing features in PHP, IMO.
-Mike
P.S. Frankly, I would prefer PHP just merge the functionality of traits and interfaces so we can use implements
or use
and only have to reference one file instead of create two parallel files, but convincing others of this approach would likely be a herculean task, so Interface Default Methods would be a not-so-bad alternative.
Back to PHP, using traits as types seems impossible, since traits are a compile-time copy/paste mechanism, which means there’s no type information available about them at runtime.
However, allowing traits to implement interfaces would solve this problem: these interfaces would be copied over to classes during compile-time, and the interface’s type information is available at runtime. On top of that, traits already have well-defined rules for conflict resolution, so we wouldn’t need any additional syntax to handle edge cases.
Traits are very ill-suited for this because everything from a trait can be overwritten:
trait T {
public function foo(string $v): int {}
}
class C {
use T;
public function foo(array $v): string {}}
This is valid code, now imagine T implements interface I:
interface I {
public foo(string $v): int;
}
C cannot implement I, moreover the conflict resolution mechanism just exposes more problems as you can rename a method from a trait, or change its visibility.
Even though it was not accepted, the interface default methods RFC approached the problem from a different angle [4].
While a majority disagreed that interfaces should implement their own methods directly, I remember it was a heavily debated topic, and believe that approaching it from the other side might be easier to accept.
One part of the contention came that this feature was proposed in June of 2023, weeks before feature freeze, and that no attempt at restricting the implementation of the default implementation was made.
I still think default methods for interfaces make more sense then trying to fix traits, but also default implementations only really make sense for interfaces with multiple methods where one is a "core" method that remains abstract and the default implementations rely on it to be implemented.
As such I am not really convinced of the utility of adding interface support for traits considering all the issues I can foresee happenning.
Best regards,
Gina P. Banyard
Hi Folks
Thanks for the replies. If interface default methods are not entirely off
the table, then I agree that's a far better option, for sure! I'll hold off
on writing my arguments for them until a thread pops up, I think it makes a
lot of sense though.
So consider the idea of traits + interfaces on hold until there's clarity
about interface default methods.
Kind regards
Brent
On Tuesday, 27 August 2024 at 09:25, Brent Roose <
brent.roose@jetbrains.com> wrote:Back to PHP, using traits as types seems impossible, since traits are a
compile-time copy/paste mechanism, which means there’s no type information
available about them at runtime.However, allowing traits to implement interfaces would solve this problem:
these interfaces would be copied over to classes during compile-time, and
the interface’s type information is available at runtime. On top of that,
traits already have well-defined rules for conflict resolution, so we
wouldn’t need any additional syntax to handle edge cases.Traits are very ill-suited for this because everything from a trait can be
overwritten:trait T {
public function foo(string $v): int {}
}class C {
use T;public function foo(array $v): string {}
}
This is valid code, now imagine T implements interface I:
interface I {
public foo(string $v): int;
}
C cannot implement I, moreover the conflict resolution mechanism just
exposes more problems as you can rename a method from a trait, or change
its visibility.
Even though it was not accepted, the interface default methods RFC
approached the problem from a different angle [4].While a majority disagreed that interfaces should implement their own
methods directly, I remember it was a heavily debated topic, and believe
that approaching it from the other side might be easier to accept.One part of the contention came that this feature was proposed in June of
2023, weeks before feature freeze, and that no attempt at restricting the
implementation of the default implementation was made.
I still think default methods for interfaces make more sense then trying
to fix traits, but also default implementations only really make sense for
interfaces with multiple methods where one is a "core" method that remains
abstract and the default implementations rely on it to be implemented.As such I am not really convinced of the utility of adding interface
support for traits considering all the issues I can foresee happenning.Best regards,
Gina P. Banyard