[Concept] Extension methods

php.internals

Alex Wells

4 years ago
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 the `map` 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: ```php // 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

Unnamed Person

4 years ago
> 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.

Deleu

4 years ago
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. > > -- > PHP Internals - PHP Runtime Development Mailing List > 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

Alex Wells

4 years ago
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 6:28 PM Deleu <deleugyn@gmail.com> wrote:

Ben Ramsey

4 years ago
On 8/10/22 09:17, Alex Wells wrote:
> 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

Paul

4 years ago
On 8/10/2022 7:17 AM, Alex Wells wrote:
> 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 the `map` 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.