rfc:user_defined_operator_overloads

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
rfc:user_defined_operator_overloads [2021/12/07 01:16] – copy edit jordanrlrfc:user_defined_operator_overloads [2022/01/17 01:16] (current) – closed voting jordanrl
Line 1: Line 1:
 ====== PHP RFC: User Defined Operator Overloads ====== ====== PHP RFC: User Defined Operator Overloads ======
-  * Version: 0.5+  * Version: 0.6
   * Date: 2021-08-14   * Date: 2021-08-14
   * Author: Jordan LeDoux, jordan.ledoux@gmail.com   * Author: Jordan LeDoux, jordan.ledoux@gmail.com
-  * Status: Under Discussion+  * Status: Declined
   * First Published at: http://wiki.php.net/rfc/user_defined_operator_overloads   * First Published at: http://wiki.php.net/rfc/user_defined_operator_overloads
  
Line 68: Line 68:
 class Collection { class Collection {
     // This function unions, it does not add     // This function unions, it does not add
-    function __add(Collection $other, bool $left) {}+    function __add(Collection $other, OperandPosition $operandPos) {}
          
     // Now the implementation doesn't suggest something incorrect     // Now the implementation doesn't suggest something incorrect
-    operator +(Collection $other, bool $left) {}+    operator +(Collection $other, OperandPosition $operandPos) {}
 } }
 </code> </code>
Line 85: Line 85:
 <code php> <code php>
 class Number { class Number {
-    public greedy operator +(Number $other, bool $left): Number {}+    public greedy operator +(Number $other, OperandPosition $operandPos): Number {}
 } }
 </code> </code>
Line 100: Line 100:
  
 This is avoided by allowing the restrictions on operator names to be separated from the restrictions on function names. This is avoided by allowing the restrictions on operator names to be separated from the restrictions on function names.
 +
 +=== Callable ===
 +
 +Operand implementations can be called on an instance of an object the way normal methods can.
 +
 +<code php>
 +// These all will work normally
 +$op = '+';
 +$callable = [$obj, '+'];
 +
 +// Calls on the object variable
 +$obj->{'+'}(1, OperandPosition::LeftSide);
 +$obj->$op(1, OperandPosition::LeftSide);
 +$callable(1, OperandPosition::LeftSide);
 +
 +// Calls using call_user_func
 +call_user_func([$obj, '+'], 1, OperandPosition::LeftSide);
 +call_user_func($callable, 1, OperandPosition::LeftSide);
 +
 +// This will error since + is not static
 +call_user_func('ObjClass::+', 1, OperandPosition::LeftSide);
 +</code>
 +
 +They can be also be directly invoked with a Closure however. This fully supports Reflection, and allows direct calls.
 +
 +<code php>
 +// Manually creating a closure allows a direct function call
 +$closure = Closure::fromCallable([$obj, '+']);
 +$closure(1, OperandPosition::LeftSide);
 +
 +// You can also retrieve the closure through Reflection
 +$closure = (new ReflectionMethod($obj, '+'))->getClosure($obj);
 +$closure(1, OperandPosition::LeftSide);
 +
 +$closure = (new ReflectionObject($obj))->getOperator('+')->getClosure($obj);
 +$closure(1, OperandPosition::LeftSide);
 +</code>
  
 ==== Add InvalidOperatorError ==== ==== Add InvalidOperatorError ====
Line 113: Line 150:
  
 Currently, this throws a TypeError. Currently, this throws a TypeError.
 +
 +==== Add OperandPosition Enum ====
 +
 +As detailed later in the RFC, letting the operator overload know which operand it is, the left operand or right operand, is critical for intelligent commutativity breaking. This is a requirement for operands such a division. To enable this a new enum has been provided, ''OperandPosition''.
 +
 +<code php>
 +enum OperandPosition {
 +    case LeftSide;
 +    case RightSide;
 +}
 +</code>
  
 ==== Add Support For Operators ==== ==== Add Support For Operators ====
  
-For most operators, the methods have the form ''operator op($other, bool $left)'' and so an implementation to support the multiply (*) operator might look like:+For most operators, the methods have the form ''operator op($other, OperandPosition $operandPos)'' and so an implementation to support the multiply (*) operator might look like:
  
 <code php> <code php>
Line 122: Line 170:
     public function __construct(public float $realPart, public float $imaginaryPart) {}     public function __construct(public float $realPart, public float $imaginaryPart) {}
          
-    operator *(int|float|ComplexNumber $other, bool $left): ComplexNumber+    operator *(int|float|ComplexNumber $other, OperandPosition $operandPos): ComplexNumber
     {     {
         if ($other instanceof ComplexNumber) {         if ($other instanceof ComplexNumber) {
Line 157: Line 205:
     function __construct(int $value) {}     function __construct(int $value) {}
  
-    operator +(Number|int $other, bool $left): Number+    operator +(Number|int $other, OperandPosition $operandPos): Number
     {     {
         if (is_int($other)) {         if (is_int($other)) {
Line 171: Line 219:
 $val1 = $num + 1; $val1 = $num + 1;
 // this is equivalent to // this is equivalent to
-// $val1 = $num->'+'(1, true);+// +(1, OperandPosition::LeftSide)
  
 $val2 = 1 + $num; $val2 = 1 + $num;
 // this is equivalent to // this is equivalent to
-// $val2 = $num->'+'(1, false);+// +(1, OperandPosition::RightSide)
 </code> </code>
  
-If the called object is the left operand, then $left is true. If the called object is the right operand, then $left is false.+If the called object is the left operand, then $operandPos is ''OperandPosition::LeftSide''. If the called object is the right operand, then $operandPos is ''OperandPosition::RightSide''.
  
 <code php> <code php>
Line 184: Line 232:
     public __construct(readonly public float $value) {}     public __construct(readonly public float $value) {}
          
-    public operator /(int|float $other, bool $left): Number +    public operator /(int|float $other, OperandPosition $operandPos): Number 
     {     {
-        if ($left) {+        if ($operandPos == OperandPosition::LeftSide) {
             $numerator = $this->value;             $numerator = $this->value;
             $denominator = $other;             $denominator = $other;
Line 212: Line 260:
  
 ^ Operator ^ Signature ^ ^ Operator ^ Signature ^
-| ''+'' | ''%%operator +($other, bool $left): mixed%%''+| ''+'' | ''%%operator +($other, OperandPosition $operandPos): mixed%%''
-| ''-'' | ''%%operator -($other, bool $left): mixed%%''+| ''-'' | ''%%operator -($other, OperandPosition $operandPos): mixed%%''
-| ''*'' | ''%%operator *($other, bool $left): mixed%%''+| ''*'' | ''%%operator *($other, OperandPosition $operandPos): mixed%%''
-| ''/'' | ''%%operator /($other, bool $left): mixed%%''+| ''/'' | ''%%operator /($other, OperandPosition $operandPos): mixed%%''
-| ''%'' | ''%%operator %($other, bool $left): mixed%%''+| ''%'' | ''%%operator %($other, OperandPosition $operandPos): mixed%%''
-| ''%%**%%'' | ''%%operator **($other, bool $left): mixed%%''+| ''%%**%%'' | ''%%operator **($other, OperandPosition $operandPos): mixed%%''
-| ''&'' | ''%%operator &($other, bool $left): mixed%%''+| ''&'' | ''%%operator &($other, OperandPosition $operandPos): mixed%%''
-| ''%%|%%'' | ''%%operator |($other, bool $left): mixed)%%''+| ''%%|%%'' | ''%%operator |($other, OperandPosition $operandPos): mixed%%''
-| ''^'' | ''%%operator ^($other, bool $left): mixed%%'' |+| ''^'' | ''%%operator ^($other, OperandPosition $operandPos): mixed%%'' |
 | ''~'' | ''%%operator ~(): mixed%%'' | | ''~'' | ''%%operator ~(): mixed%%'' |
-| ''<<'' | ''%%operator <<($other, bool $left): mixed%%''+| ''<<'' | ''%%operator <<($other, OperandPosition $operandPos): mixed%%''
-| ''>>'' | ''%%operator >>($other, bool $left): mixed%%'' |+| ''>>'' | ''%%operator >>($other, OperandPosition $operandPos): mixed%%'' |
 | ''=='' | ''%%operator ==($other): bool%%'' | | ''=='' | ''%%operator ==($other): bool%%'' |
 | ''<=>'' | ''%%operator <=>($other): int%%'' | | ''<=>'' | ''%%operator <=>($other): int%%'' |
Line 242: Line 290:
     }     }
          
-    public operator +(int|float|string|BigNumber $other, bool $left): self+    public operator +(int|float|string|BigNumber $other, OperandPosition $operandPos): self
     {     {
         if ($other instanceof BigNumber) {         if ($other instanceof BigNumber) {
Line 306: Line 354:
 ==== Notable Operators ==== ==== Notable Operators ====
  
-Most of the operators follow the form ''operator op($other, bool $left): mixed'' and those all behave in the same way. There are a few operators that will have a different signature, and/or behave differently.+Most of the operators follow the form ''operator op($other, OperandPosition $operandPos): mixed'' and those all behave in the same way. There are a few operators that will have a different signature, and/or behave differently.
  
 === Bitwise Not Operator (~) === === Bitwise Not Operator (~) ===
Line 314: Line 362:
 === Equals Operator (==) === === Equals Operator (==) ===
  
-Because comparisons have a reflection relationship instead of a commutative one, the $left argument is omitted as it could only be used for evil (making ''$obj == 5'' have a different result than ''5 == $obj'').+Because comparisons have a reflection relationship instead of a commutative one, the $operandPos argument is omitted as it could only be used for evil (making ''$obj == 5'' have a different result than ''5 == $obj'').
  
-Comparison operators do not throw the ''InvalidOperatorError'' when unimplemented. Instead, the PHP engine falls back to existing comparison logic in the absence of an override for a given class.+Equality and comparison operators do not throw the ''InvalidOperatorError'' when unimplemented. Instead, the PHP engine falls back to existing comparison logic in the absence of an override for a given class.
  
 The signature for the equals operator has the additional restriction of returning ''bool'' instead of ''mixed''. The signature for the equals operator has the additional restriction of returning ''bool'' instead of ''mixed''.
Line 330: Line 378:
 Any return value larger than 0 will be normalized to 1, and any return value smaller than 0 will be normalized to -1. Any return value larger than 0 will be normalized to 1, and any return value smaller than 0 will be normalized to -1.
  
-The $left argument is omitted as it could only be used for evil e.g. implementing different comparison logic depending on which side its on. Instead of passing $left the engine will multiply the result of the call by (-1) where appropriate:+The $operandPos argument is omitted as it could only be used for evil e.g. implementing different comparison logic depending on which side it'on. Instead of passing $operandPos the engine will multiply the result of the call by (-1) where appropriate:
  
 <code php> <code php>
Line 349: Line 397:
 $obj = new Number(5); $obj = new Number(5);
  
-$less_than = ($obj < 5);+$less_than = $obj < 5;
 // is equivalent to // is equivalent to
-// $less_than = ($obj->'<=>'(5) === -1);+// (<=>(5) === -1);
  
 $greater_than = 5 > $obj; $greater_than = 5 > $obj;
 // is equivalent to // is equivalent to
-// $greater_than = ( ($obj->'<=>'(5) * - 1) === -1 );+// ( (<=>(5) * - 1) === -1 );
 </code> </code>
  
Line 374: Line 422:
 <code php> <code php>
 class Matrix { class Matrix {
-    operator +($other, bool $left): Matrix {}+    operator +($other, OperandPosition $operandPos): Matrix {}
 } }
  
Line 381: Line 429:
  
 The ''mixed'' type can still be used for the ''$other'' parameter, but it must do so by explicitly typing it as such. The ''mixed'' type can still be used for the ''$other'' parameter, but it must do so by explicitly typing it as such.
 +
 +==== Attributes ====
 +
 +A new target ''Attribute::TARGET_OPERATOR'' is added to allow attributes to specifically target operator implementations.
 +
 +==== Reflection ====
 +
 +Several changes to reflection must be made to support this feature.
 +
 +=== Changes To ReflectionClass ===
 +
 +== Changes to getMethods(), getMethod(), and hasMethod() ==
 +
 +These methods need to be updated to ignore the operator methods. Since these are stored internally like any other function on the class entry, they need to be filtered from the results.
 +
 +The reason for removing the operators from this result is because the operator methods are not callable with string literals on the object. Since they cannot be called like a method is, they should not be returned with the other methods on a class.
 +
 +== Adding getOperators(), getOperator(), and hasOperator() ==
 +
 +These methods must be added to interact with the function handlers for the operator implementations. They will act as an inverse to the changes above.
 +
 +Operator methods will be represented by an instance of ''ReflectionMethod'', since in most respects they can be treated like a normal method for the purposes of reflection.
 +
 +=== Changes To ReflectionMethod ===
 +
 +== New Method isOperator() ==
 +
 +Returns true if the method being reflected uses the ''operator'' keyword. Returns false otherwise.
  
 ===== FAQ ===== ===== FAQ =====
Line 400: Line 476:
 Language design shouldn't focus on preventing people from doing things you disagree with, at the expense of blocking appropriate usage of a feature. Language design shouldn't focus on preventing people from doing things you disagree with, at the expense of blocking appropriate usage of a feature.
  
-==== When will $left be useful? ====+==== When will $operandPos be useful? ====
  
 Not all operators are commutative. The most trivial example of this is with subtraction: Not all operators are commutative. The most trivial example of this is with subtraction:
Line 408: Line 484:
     public function __construct(readonly public int|float $value) {}     public function __construct(readonly public int|float $value) {}
          
-    public operator -(int|float $other, bool $left): Number+    public operator -(int|float $other, OperandPosition $operandPos): Number
     {     {
-        if ($left) {+        if ($operandPos == OperandPosition::LeftSide) {
             return new Number($this->value - $other);             return new Number($this->value - $other);
         } else {         } else {
Line 425: Line 501:
     public function __construct(readonly public array $value) {}     public function __construct(readonly public array $value) {}
          
-    public operator *(Matrix $other, bool $left): Number+    public operator *(Matrix $other, OperandPosition $operandPos): Number
     {     {
-        if ($left) {+        if ($operandPos == OperandPosition::LeftSide) {
             // Count of my columns needs to match             // Count of my columns needs to match
             // count of $other rows             // count of $other rows
Line 447: Line 523:
 interface Addable interface Addable
 { {
-    operator +(mixed $other, bool $left): mixed+    operator +(mixed $other, OperandPosition $operandPos): mixed
 } }
  
Line 469: Line 545:
  
 <code php> <code php>
-function processMoneyValues(Money $leftMoney $right) +class Money { 
-{ +    operator +(Money $otherOperandPosition $operandPos): Money {}
-    return $left + $right;+
 } }
  
-processMoneyValues( +$result = new Money(5, 'USD'new Vector2d(5, 10);
-    new Money(5, 'USD')+
-    new Vector2d(5, 10+
-);+
  
 // Type error, Vector2d can't be used as Money // Type error, Vector2d can't be used as Money
 </code> </code>
  
-This can also be caught by typing the arguments to the operator implementations themselves.+This can also be caught by typing the arguments to a helper function:
  
 <code php> <code php>
-class Money { +function processMoneyValues(Money $leftMoney $right) 
-    operator +(Money $otherbool $left): Money {}+{ 
 +    return $left + $right;
 } }
  
-$result = new Money(5, 'USD'new Vextor2d(5, 10);+processMoneyValues( 
 +    new Money(5, 'USD')
 +    new Vector2d(5, 10
 +);
  
 // Type error, Vector2d can't be used as Money // Type error, Vector2d can't be used as Money
Line 525: Line 601:
 // Parses just fine and implements behavior // Parses just fine and implements behavior
 class SomeClass { class SomeClass {
-    public local operator +(mixed $other, bool $left): SomeClass {}+    public local operator +(mixed $other, OperandPosition $operandPos): SomeClass {}
 } }
  
Line 563: Line 639:
 Existing extensions can continue to define their own operator overloads by providing a ''do_operation'' call for their classes, however classes which are open to be extended may benefit from being updated so that their overloads can be extended by implementing the necessary methods. In order to accomplish this, the extension class would need to call ''zend_std_call_op_override'' at the start of its ''do_operation'' handler. The operator override handler will not throw the ''InvalidOperatorError'' if the class entry has the type ''ZEND_INTERNAL_CLASS''. Instead, it will return the value ''FAILURE'' of type ''zend_result'' Existing extensions can continue to define their own operator overloads by providing a ''do_operation'' call for their classes, however classes which are open to be extended may benefit from being updated so that their overloads can be extended by implementing the necessary methods. In order to accomplish this, the extension class would need to call ''zend_std_call_op_override'' at the start of its ''do_operation'' handler. The operator override handler will not throw the ''InvalidOperatorError'' if the class entry has the type ''ZEND_INTERNAL_CLASS''. Instead, it will return the value ''FAILURE'' of type ''zend_result''
  
-Thus, the following code at start of any extension's ''do_operation'' handler would be the minimal sufficient addition:+Thus, the following code at the start of any extension's ''do_operation'' handler would be the minimal sufficient addition:
  
 <code c> <code c>
Line 572: Line 648:
 // The rest of the extension's do_operation handler // The rest of the extension's do_operation handler
 </code> </code>
 +
 +To further help extensions support this feature, there are two helper functions:
 +
 +<code c>
 +int has_overload = zend_std_has_op_overload(opcode, &zval);
 +
 +zend_function overload_method = zend_std_get_op_overload(opcode, &ce);
 +</code>
 +
 +It is safe to pass any zval pointer to ''zend_std_has_op_overload()'', as it first checks whether or not the ''Z_TYPE_P(zval) == IS_OBJECT'' and returns 0 if it doesn't.
  
 ==== To Opcache ==== ==== To Opcache ====
Line 597: Line 683:
 This RFC deals with allowing each class to define its own interaction with operators. However, if overloading the operator itself were desired for the entire application, a different approach would be needed. This is also something that the ''operator'' keyword future proofs against, but is not an intended proposal of this RFC author. This RFC deals with allowing each class to define its own interaction with operators. However, if overloading the operator itself were desired for the entire application, a different approach would be needed. This is also something that the ''operator'' keyword future proofs against, but is not an intended proposal of this RFC author.
  
-===== Proposed Voting Choices ===== +==== Functions for Operators ==== 
-Add limited user-defined operator overloads as describedyes/noA 2/3 vote is required to pass+Having functions for operators may be beneficial when objects which use operator overloads are used in conjunction with functions like ''array_reduce''. For example: 
 + 
 +<code php> 
 +array_reduce($arrOfObjs, +(...)); 
 +</code> 
 + 
 +These could be polyfilled in PHP currently: 
 + 
 +<code php> 
 +array_reduce($arrOfObjs, fn ($a, $b) => ($a + $b)); 
 +</code> 
 + 
 +==== Query Builder Improvements ==== 
 +With some additional improvements, it's possible that operator overloads could provide some very useful tools for things such as query builders: 
 + 
 +<code php> 
 +$qb->select(Product::class)->where(Price::class < 50); 
 +</code> 
 + 
 +==== Enum Return Type For <=> ==== 
 + 
 +Returning an enum for the <=> would be preferable for two reasons. 
 + 
 +  - It allows the function to return an equivalent of 'uncomparable' (where all variations are false) 
 +  - It is easier to read and understand the behavior in code, while integer values often require a moment to remember the meaning 
 + 
 +This is listed as future scope because there is a separate RFC which covers this feature: https://wiki.php.net/rfc/sorting_enum 
 + 
 +It is listed as a separate RFC because it is something that could be delivered whether or not this RFC passes.
  
 ===== Patches and Tests ===== ===== Patches and Tests =====
Line 610: Line 724:
   - a link to the language specification section (if any)   - a link to the language specification section (if any)
  
-===== References =====+===== Proposed Voting Choices ===== 
 +Add limited user-defined operator overloads as described: yes/no. A 2/3 vote is required to pass.  
 + 
 +===== Vote ===== 
 + 
 +Voting started 2022-01-03 at 00:15 UTC and will end 2022-01-17 at 00:15 UTC.
  
 +<doodle title="Adopt user defined operator overloads as described?" auth="jordanrl" voteType="single" closed="true">
 +   * Yes
 +   * No
 +</doodle>
  
 ===== Changelog ===== ===== Changelog =====
Line 619: Line 742:
   * 0.4: Added section on opcode changes   * 0.4: Added section on opcode changes
   * 0.5: Simplified and cleaned up RFC; moved to ''operator op()'' format   * 0.5: Simplified and cleaned up RFC; moved to ''operator op()'' format
 +  * 0.6: Added OperandPosition Enum
rfc/user_defined_operator_overloads.1638839814.txt.gz · Last modified: 2021/12/07 01:16 by jordanrl