Nearly three years ago, PHP RFC: User Defined Operator Overloads was declined due to scope and new syntax concerns. However, the GMP class, which represents integer numbers in the GMP extension, was (accidentally) left non-final. This RFC explores the potential of extending GMP with a limited form of operator overriding, providing cleaner expressions for mathematical constructs and allowing new types of numerical representations such as units, complex numbers, and more.
This is to integers as ArrayAccess is to arrays, and Stringable is to strings, providing a simplified framework for defining integer mathematics, such as units.
This is not a full operator overrides implementation; it is much simpler and it does not stop us from implementing that in the future. It, does, however seek to be a polar opposite of the PHP RFC: User Defined Operator Overloads RFC.
Several curious people have wondered why this is focused on the GMP extension. The reasons are quite simple:
The \GMP class will be changed to the following signature:
readonly class GMP { public function __construct(int|string|GMP $num = 0, int $base = 0) {} public function __serialize(): array {} public function __unserialize(array $data): void {} protected function add(GMP|int|string $left, GMP|int|string $right): GMP {} protected function multiply(GMP|int|string $left, GMP|int|string $right): GMP {} protected function subtract(GMP|int|string $left, GMP|int|string $right): GMP {} protected function divide(GMP|int|string $left, GMP|int|string $right): GMP {} protected function mod(GMP|int|string $left, GMP|int|string $right): GMP {} protected function pow(GMP|int|string $base, GMP|int|string $exp): GMP {} protected function comparable(GMP|int|string $op1, GMP|int|string $op2): bool {} }
The changes are listed below:
readonly
to prevent developers from using state in inherited types.string|int
public
.For any existing GMP code, absolutely nothing changes. The extension's behavior ONLY changes when the developer extends the GMP class and uses it mathematically. To be usable, the developer must override the desired operations and make them public. This ensures the class can be used even if the GMP extension is unavailable. A polyfill is included in the example below, which can be included in libraries that use this functionality.
Note the lack of equals, less-than, and greater-than operators. These are deliberately left out and replaced by a “comparable” method where the developer can indicate whether the two objects are compatible. They are left out for several reasons:
Shift and bitwise operations are also left out of the class because it is expected that these behaviors won't change for any type of number that could be implemented here.
Below is a listing showing a polyfill, a Duration class that only allows adding, multiplying, and exponentiation with scalars and is only comparable to Durations. Further, it cannot be negative.
<?php if(!class_exists('GMP')) { // polyfill for completion readonly class GMP { public function __construct(private int|string|GMP $num) {} protected function add(GMP|int|string $left, GMP|int|string $right): GMP { return new self($left?->num + $right?->num); } protected function multiply(GMP|int|string $left, GMP|int|string $right): GMP { return new self($left?->num * $right?->num); } protected function subtract(GMP|int|string $left, GMP|int|string $right): GMP { return new self($left?->num - $right?->num); } protected function divide(GMP|int|string $left, GMP|int|string $right): GMP { return new self($left?->num / $right?->num); } protected function mod(GMP|int|string $left, GMP|int|string $right): GMP { return new self($left?->num % $right?->num); } protected function pow(GMP|int|string $base, GMP|int|string $exp): GMP { return new self($base?->num ** $exp?->num); } protected function comparable(GMP|int|string $op1, GMP|int|string $op2): bool { return is_numeric($op1) && is_numeric($op2); } } } readonly class Duration extends GMP { public function __construct(int|string|GMP $num = 0) { if($num < 0) { throw new ArithmeticError('Duration cannot be negative.'); } parent::__construct($num); } private function assertValidScalar(mixed $maybeScalar, string $operation): void { if(is_numeric($maybeScalar)) { return; } if(get_class($maybeScalar) === 'GMP') { return; } throw new ValueError("Can only perform $operation on GMP or int. Got " . get_debug_type($maybeScalar) . " instead."); } private function guardScalars(GMP|int|string $left, GMP|int|string $right, string $operation): void { if($left === $this) { $this->assertValidScalar($right, $operation); return; } if($right === $this) { $this->assertValidScalar($left, $operation); return; } throw new LogicException('$left or $right must be the same instance as $this.'); } private function getResult(GMP $result): self { return new self($result); } public function add(GMP|int|string $left, GMP|int|string $right): static { $this->guardScalars($left, $right, 'addition'); return $this->getResult(parent::add($left, $right)); } public function multiply(GMP|int|string $left, GMP|int|string $right): GMP { $this->guardScalars($left, $right, 'multiplication'); return $this->getResult(parent::multiply($left, $right)); } public function pow(GMP|int|string $base, GMP|int|string $exp): GMP { $this->assertValidScalar($exp, 'exponentiation'); if($base !== $this) { throw new LogicException('$base must be the same instance as $this.'); } return $this->getResult(parent::pow($base, $exp)); } public function comparable(GMP|int|string $op1, GMP|int|string $op2): bool { return $op1 instanceof self && $op2 instanceof self; } } $duration = new Duration(10); $other = new Duration(200); $regular = new GMP(10); function do_op($description, $op): void { global $duration, $other, $regular; try { $result = eval('return ' . $op . ';'); echo "$description: $op = $result" . PHP_EOL; } catch (Throwable $exception) { echo "$description: $op = [" . get_class($exception) . ': ' . $exception->getMessage() . ']' . PHP_EOL; } } do_op('Duration', '$duration'); do_op('Regular', '$regular'); do_op('Other', '$other'); do_op('Regular', '$regular + 10'); do_op('Duration', '$duration + 10'); do_op('Duration + Duration', '$duration + $other'); do_op('Duration + Regular', '$duration + $regular'); do_op('Division not allowed', '$duration / 10'); do_op('Multiplication', '$regular * 10'); do_op('No negatives', '$duration + -20'); do_op('Comparison', '$duration < $other'); do_op('Comparison failure', '$duration < 20'); /** * Output: * Duration: $duration = 10 * Regular: $regular = 10 * Other: $other = 200 * Regular: $regular + 10 = 20 * Duration: $duration + 10 = 20 * Duration + Duration: $duration + $other = [ValueError: Can only perform addition on GMP or int. Got Duration instead.] * Duration + Regular: $duration + $regular = 20 * Division not allowed: $duration / 10 = [Error: Invalid callback Duration::divide, cannot access protected method Duration::divide()] * Multiplication: $regular * 10 = 100 * No negatives: $duration + -20 = [ArithmeticError: Duration cannot be negative.] * Comparison: $duration < $other = 1 * Comparison failure: $duration < 20 = [ArithmeticError: Can't compare incompatible types] */
The following exceptions can be thrown by the engine while attempting to perform mathematical operations:
Additionally, the implementor may throw additional exceptions, such as a ValueError in the example above, to indicate that something is not possible to the developer.
There are no backward incompatible changes. Existing GMP-based code will remain unaffected.
8.4 if time allows, or the next version.
No impact.
Only GMP will be affected.
There should be no impact to Opcache.
None, yet.
Code using the GMP extension without extending it will remain unchanged.
Prototype patch: PR 14730
After the project is implemented, this section should contain
Links to external references, discussions or RFCs
On paper, a separate extension sounds like a good idea. However, I've attempted this with varying degrees of success. Reimplementing mathematics is not straightforward or a good idea when libraries like GMP do a much better job. Since there is already a GMP extension, it makes sense to merge it with that extension instead of forking it. If this RFC is rejected, it is rather straightforward to fork the extension and create a new one. However, that will certainly cause problems in environments where the standard GMP extension is installed instead.
Some people on the list have voiced their opinion that we should revisit full operator overloading. This RFC does not prevent us from revisiting that in the future, but it is outside the scope of this RFC.
There are many more operators that could be implemented. However, another concern was raised that people may “abuse” this feature to implement arbitrary objects that do strange things with operators. Thus, the scope is kept to “obviously integer-y things,” and the GMP extension goes to great lengths to have the engine treat its objects as numbers.