====== Scalar extensions RFC ====== * Date: 2020-05-07 * Author: Ilija Tovilo, tovilo.ilija@gmail.com * Status: Under discussion * Target Version: PHP 8.0 * Implementation: https://github.com/php/php-src/pull/5535 ===== Proposal ===== This RFC is based on the [[https://github.com/nikic/scalar_objects|scalar objects extension]]. Scalar extensions allow extending scalar types with methods. The motivation for this RFC is readability and the opportunity for a cleaner standard library. use extension string StringExtension; use extension array ArrayExtension; class StringExtension { public static function split($self, $separator) { return explode($separator, $self); } } class ArrayExtension { public static function map($self, $callable) { return array_map($callable, $self); } public static function join($self, $separator) { return implode($separator, $self); } } $x = '1, 2, 3, 4' ->split(', ') ->map(fn($y) => $y * 2) ->join(', '); // $x = '2, 4, 6, 8' ===== Readability ===== ==== Reading flow ==== Currently libraries provides functions that operate on scalar values. Examples are ''%%array_map%%'', ''%%explode%%'', ''%%preg_match%%'', etc. When you start nesting these function calls the code can become very unreadable, especially when the functions accept multiple parameters. Compare the following two code snippets. $x = range(1, 10) ->filter(fn($y) => $y % 2 === 0) ->map(fn($y) => $y * 2); // vs $x = array_map( fn($y) => $y * 2, array_filter( range(1, 10), fn($x) => $x % 2 === 0 ) ); Nested function calls have to be read from the inside out. Chaining the methods can be read from left to right / top to bottom. ==== Needle/haystack ==== Chaining methods also automatically solves the needle/haystack problem. // Which one is the needle again? str_contains('foo', 'f'); strpos('foo', 'f'); // Ah, much more obvious 'foo'->contains('f'); 'foo'->indexOf('f'); ==== Function prefix ==== Function names are usually prefixed by the scalar type (''%%string_%%'', ''%%str%%'', ''%%array_%%'', etc.) to make them unique. Scalar extensions make prefixes unnecessary as the extension is restricted to a given type. // No need to array_/string_ prefix, it's clear from the operand type ['foo']->contains('foo'); 'foo'->contains('f'); ===== Scoping ===== ''%%use extension%%'' is only applied to the current file. This will allow seamless integration of libraries that might be using different scalar extensions. ''%%use extension%%'' is only valid at the top of the file. // file1.php use extension string FooStringExtension; 'foo'->foo(); 'foo'->bar(); // Not valid here // file2.php use extension string BarStringExtension; 'bar'->bar(); 'bar'->foo(); // Not valid here ===== Multiple handlers ===== You can register multiple handlers per type. They will be tried in sequence until a method with the given name is found. use extension string FooStringExtension; use extension string BarStringExtension; class FooStringExtension { public function foo($self) {} public function both($self) {} } class BarStringExtension { public function bar($self) {} public function both($self) {} } 'foo'->foo(); // Ok 'bar'->bar(); // Ok 'both'->both(); // FooStringExtension::both called 'baz'->baz(); // Error: Call to undefined method string::baz() ===== callStatic ===== The handlers ''%%__callStatic%%'' method will be called if it is implemented. use extension string FooStringExtension; class DynamicStringExtension { public function foo($self) {} public function __callStatic($self) {} } 'foo'->foo(); // DynamicStringExtension::foo called 'bar'->bar(); // DynamicStringExtension::__callStatic called ===== By value/reference ===== Scalars are normally passed by value. The same goes for scalar extensions. Modifying ''%%$self%%'' in a scalar extension does not modify the original value. To actually modify the original value you need to pass ''%%$self%%'' by reference using ''%%&%%''. use extension array ArrayExtension; class ArrayExtension { public static function append($self, $value) { $self[] = $value; } public static function appendByRef(&$self, $value) { $self[] = $value; } } $x = []; $x->append('foo'); // $x is still empty $x->appendByRef('foo'); // Now $x is ['foo'] ===== Autoloading ===== The autoloading of the extension class is only triggered when a method is called on a value of the given type. use extension array ArrayExtension; // ArrayExtension is registered as an extension but not loaded $x = []; $x->anything(); // Only now does PHP look for a class named ArrayExtension ===== Performance overhead ===== There is no performance overhead to existing method calls. Scalar extensions are only triggered if the value is a scalar. ===== Future scope ===== ==== Standard library ==== This RFC provides a big opportunity to provide a better standard library for scalar types. Designing and implementing a standard library is a large undertaking and out of scope for a single proposal. It’s important we don’t rush it as we’ll have to live with this API for a long time. Each component should be introduced in a separate RFC. This RFC does not provide a standard library. But it’s an invite for other proposals to add build the standard library bit by bit. **All examples in this RFC are only examples.** ==== Extensions for other types ==== We might want to allow extending other types like classes and interfaces in the future. use Acme\Foo; use extension Foo FooExtension; class FooExtension { function bar($self) {} } $foo = new Foo(); $foo->bar(); ===== Backward Incompatible Changes ===== There are no backward incompatible changes in this RFC.