Hey internals.
The idea is to introduce extension methods, similar to those in Kotlin, C#,
Dart. For those unfamiliar, those are just regular functions with fancy
syntax. However, I think having those will not only improve readability,
but also cover some of the previously requested features.
Say you have a class Collection
and you want to add a new method
map(callable $callable): Collection
. The first instinct is to go into
that file and add the method, this will work. But what if class Collection
is defined in a vendor package? You could define a regular
function like this: map(Collection $collection, callable $callable): Collection
, then import it whenever you need to use that.
This solution works, but in practice is rarely used. The reasons are:
- there's no IDE completion:
$collection->
<- here I want IDE to
auto-complete themap
method somehow, but since it's a function this is
impossible - it's ugly looking and hard to read:
getPromotions(mostExpensiveItem(getShoppingList(getCurrentUser(), 'wishlist'), ['exclude' => 'onSale']), $holiday);
- it's easy to mess up the order of arguments
The pipe operator RFC [1] was trying to solve that but got rejected.
Major libraries/frameworks like Laravel [2] and Carbon [3] use a solution
with __call
that allows users to define custom methods on their classes:
Carbon::mixin('toMyTimeFormat', fn () => $this->format('MM YYYY DD'));
Again, this solution:
- doesn't offer IDE completion and doesn't offer IDE navigation
- harder to understand
- doesn't work with interfaces, traits, enums or primitives
Other languages (Kotlin [4], C# [5], Dart [6]) I'm aware of solve this
problem with extension methods. All of them use slightly different syntax,
but the main idea is:
- you define an extension method the same way you define a function, except
you specify which type you're extending - you can use any type that a function can accept. This includes
primitives, classes, interfaces, traits and enums - the type you're extending is implicitly bound to
$this
- you only have access to the public scope - you can't access
private/protected members - you have to import those the same way you import functions. You can't
define extensions globally
In PHP this could look like this:
// Illuminate/Collection.php
namespace Illuminate;
class Collection {}
// App/CollectionExtension.php
namespace App;
use Illuminate\Collection;
extension CollectionExtension on Collection {
function map(callable $callable): Collection {
return new Collection(array_map($callable, $this->items));
}
}
// App/Business/Logic.php
namespace App\Business;
use extension App\CollectionExtension::map;
(new Collection(1, 2, 3))
->map(fn ($value) => $value + 1)
->map(fn ($value) => $value * 2);
The way this should work is PHP first checks whether Collection
has map
method. If one's missing, it gets all used extensions that match that
method name, autoloads them (the same way other class-likes are loaded) and
checks whether current type Collection
matches the type specified in the
extension. If so, calls the method, otherwise attempts to call __call.
This same concept eliminates the need for scalar objects or scalar
extension methods [7].
I'm guessing there will be problems with optimization and OPCache.
What are your thoughts?
References:
- [1] - https://wiki.php.net/rfc/pipe-operator-v2
- [2] -
https://github.com/laravel/framework/blob/9.x/src/Illuminate/Macroable/Traits/Macroable.php - [3] -
https://github.com/briannesbitt/Carbon/blob/master/src/Carbon/Traits/Macro.php - [4] - https://kotlinlang.org/docs/extensions.html
- [5] -
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods - [6] - https://dart.dev/guides/language/extension-methods
- [7] - https://github.com/nikic/scalar_objects
What are your thoughts?
It's a fantastic feature that I've used in Rust, although there are
some differences. First, it doesn't work for regular methods -- they
have to be part of a trait. Secondly, in general a trait can only be
implemented for a given type if it is in the package which defines
that type, or in the package which defines the trait. Third, the trait
must be in scope. These rules help people understand where the methods
are coming from, which is particularly helpful for large code bases or
teams.
PHP doesn't really have tools for these kinds of restrictions, but I
think it's necessary. You'd need some way to manage where extension
methods are loaded, how they are found, and without pessimizing
performance of method calls in general just because this feature
exists.
On Wed, Aug 10, 2022 at 5:16 PM Levi Morrison via internals <
internals@lists.php.net> wrote:
What are your thoughts?
It's a fantastic feature that I've used in Rust, although there are
some differences. First, it doesn't work for regular methods -- they
have to be part of a trait. Secondly, in general a trait can only be
implemented for a given type if it is in the package which defines
that type, or in the package which defines the trait. Third, the trait
must be in scope. These rules help people understand where the methods
are coming from, which is particularly helpful for large code bases or
teams.PHP doesn't really have tools for these kinds of restrictions, but I
think it's necessary. You'd need some way to manage where extension
methods are loaded, how they are found, and without pessimizing
performance of method calls in general just because this feature
exists.--
To unsubscribe, visit: https://www.php.net/unsub.php
As I was thinking about how this feature would be cool, I was also worried
about how big of a mess it could become, given the lack of restrictions you
pointed out here. However, knowing how PHP works, I wonder if the following
could be made possible:
// Vendor File
namespace Illuminate\Support;
class Collection {}
// Extension File
namespace App\Whatever;
extension LaravelCollection on Collection {}
// Usage File
namespace App\Business;
use App\Whatever\Laravelcollection;
$collection = (new Collection())->extensionMethodAvailableHere();
The goal here is to
1- Disallow use Class; use ExtensionClass
simultaneously (conflicting
symbols on compile-time?)
2- Bind the Base-class Symbol through the ExtensionClass symbol
3- Disallow two extensions to compete with each other
4- The user would always know that a symbol has a method either via 1-level
extension or from the original class directly - it doesn't come from
unknown places
I feel like this would be powerful enough to solve a lot of usability on
PHP OOP while not being crazy enough to create a nightmare on codebases and
the internals of the PHP Engine. Does this make sense?
--
Marco Deleu
I believe disallowing multiple extensions on one type defeats one of
the purposes of the feature - extending from outside. Let's say you have a
vendor package for manipulating strings which defines an extension on
string
type. It works, but then you need one more custom extensions -
some kind of replaceLastRegex
method. You define it in your own
extension, but then you're either missing the vendor package methods or
your own extensions. This might even make people avoid extensions, because
there would be no way to use both extensions, hence making them extract
those into functions.
I think that if the goal is to avoid the confusion and/or mess, we could
force specifying the when using the extension. It would then be crystal
clear where the method is coming from and also it'd be trivial to check
whether method names are conflicting between extensions. The syntax is just
a demonstration: use extension App\Whatever\CollectionExtension as Illuminate\Collection::map;
On Wed, Aug 10, 2022 at 5:16 PM Levi Morrison via internals <
internals@lists.php.net> wrote:What are your thoughts?
It's a fantastic feature that I've used in Rust, although there are
some differences. First, it doesn't work for regular methods -- they
have to be part of a trait. Secondly, in general a trait can only be
implemented for a given type if it is in the package which defines
that type, or in the package which defines the trait. Third, the trait
must be in scope. These rules help people understand where the methods
are coming from, which is particularly helpful for large code bases or
teams.PHP doesn't really have tools for these kinds of restrictions, but I
think it's necessary. You'd need some way to manage where extension
methods are loaded, how they are found, and without pessimizing
performance of method calls in general just because this feature
exists.--
To unsubscribe, visit: https://www.php.net/unsub.php
As I was thinking about how this feature would be cool, I was also worried
about how big of a mess it could become, given the lack of restrictions you
pointed out here. However, knowing how PHP works, I wonder if the following
could be made possible:// Vendor File namespace Illuminate\Support; class Collection {} // Extension File namespace App\Whatever; extension LaravelCollection on Collection {} // Usage File namespace App\Business; use App\Whatever\Laravelcollection; $collection = (new Collection())->extensionMethodAvailableHere();
The goal here is to
1- Disallowuse Class; use ExtensionClass
simultaneously (conflicting
symbols on compile-time?)
2- Bind the Base-class Symbol through the ExtensionClass symbol
3- Disallow two extensions to compete with each other
4- The user would always know that a symbol has a method either via
1-level extension or from the original class directly - it doesn't come
from unknown placesI feel like this would be powerful enough to solve a lot of usability on
PHP OOP while not being crazy enough to create a nightmare on codebases and
the internals of the PHP Engine. Does this make sense?--
Marco Deleu
The idea is to introduce extension methods, similar to those in Kotlin, C#,
Dart. For those unfamiliar, those are just regular functions with fancy
syntax. However, I think having those will not only improve readability,
but also cover some of the previously requested features.
Other languages (Kotlin [4], C# [5], Dart [6]) I'm aware of solve this
problem with extension methods. All of them use slightly different syntax,
but the main idea is:
- you define an extension method the same way you define a function, except
you specify which type you're extending- you can use any type that a function can accept. This includes
primitives, classes, interfaces, traits and enums- the type you're extending is implicitly bound to
$this
- you only have access to the public scope - you can't access
private/protected members- you have to import those the same way you import functions. You can't
define extensions globally
I believe this is also called "monkey patching" in some places, and
Ruby, Python, and JavaScript all offer some form of object extension
similar to this.
There is also the PHP runkit extension that provides some of the
functionality you've described: https://www.php.net/runkit7
--
Cheers,
Ben
This solution works, but in practice is rarely used. The reasons are:
- there's no IDE completion:
$collection->
<- here I want IDE to
auto-complete themap
method somehow, but since it's a function this is
impossible
This isn't impossible. There's nothing stopping an IDE from seeing
$collection->
and suggesting/completing that with map($collection,
- they just don't right now. Offhand I don't see why this would be more
difficult for them to implement than support for extension methods. As a
bonus it'd work with existing functions.
- it's ugly looking and hard to read:
getPromotions(mostExpensiveItem(getShoppingList(getCurrentUser(), 'wishlist'), ['exclude' => 'onSale']), $holiday);
It's easy to write ugly looking code using any particular syntax. It's
unconvincing to do so. Instead of rehashing it all here I'd suggest
looking back at examples and counter-examples of exactly this sort of
thing in past pipe operator discussions.
In short every ugly example has a fine, easy-to-read way of writing it
now in existing code. A useful readability comparison would pit the new
feature against the best looking version of example code you can come up
with rather than the worst. Without that you're not really demonstrating
improvement.
The most an ugly example has done for a proposal is generate noise.
- it's easy to mess up the order of arguments
An extension method would shift a single argument from inside
parentheses to the left of the function name. It just moves it. I don't
see any impact here.
The pipe operator RFC [1] was trying to solve that but got rejected.
This is essentially making ->
the pipe operator with extra steps
(extension
/use extension
) and less utility (not working on existing
functions.)
Considering all the conflict resolution stuff would have to be done
anyway you may as well drop the extra steps and explore ->
as a pipe
operator directly. Only add in extension/use extension if there's a
blocker they solve - i.e. when they provide real value.