On Sat, Sep 14, 2024, at 4:48 PM, Jordan LeDoux wrote: > Hello internals, > > > This discussion will use my previous RFC as the starting point for > conversation: https://wiki.php.net/rfc/user_defined_operator_overloads
Replying to the top to avoid dragging any particular side discussion into this... I've seen a few people, both in this thread and previously, make the argument that "operator overloads are only meaningful for math, and seriously no one does fancy math in PHP, so operator overloads are not necessary." This statement is patently false, and I would ask that everyone stop using it as it is simply FUD. I don't mean just the "no one does fancy math in PHP", but "operators are only meaningful for math" is just an ignorant statement. As someone asked for use cases, here's a few use cases for operator overloads that do not fall into the category of fancy numeric math. Jordan, feel free to borrow any of these verbatim for your next RFC draft if you wish. (I naturally haven't tried running any of them, so forgive any typos or bugs. It's just to get the point across of each use case.) ## Pathlib As I mentioned previously, Python's pathlib uses / to join different path fragments together. In PHP, one could implement such a library very simply with overloads. (A not-late-night implementation would probably be more performant than this, but it's just a POC.) class Path implements Stringable { private array $parts; public function __construct(?string $path = null) { $this->parts = array_filter(explode('/', $path ?? '')); } public static function fromArray(array $parts): self { $new = new self(); $new->parts = $parts; return $new; } public function __toString() { return implode('/', $this->parts); } operator /(Path|string $other, OperandPosition $pos): Path { if ($other instanceof Path) { $other = (string)$other; } $otherParts = array_filter(explode('/', $path)); return match ($pos) { OperandPosition::LeftSide => self::fromArray([...$this->parts, ...$otherParts]), OperandPosition::RightSide => self::fromArray([...$otherParts, ...$this->parts]), }; } } $p = new Path('/foo/bar'); $p2 = $p / 'beep' / 'narf/poink'; ## Collections In my research into collections in other languages, I found it was extremely common for collections to have operator overloads on them. Rather than repeat it here, I will just link to my results and recommendations for what operators would make sense for what operation: https://github.com/Crell/php-rfcs/blob/master/collections/research-notes.md#operator-based-operations ## Enum sets Ideally, we would just use generic collections for this directly. However, even without generics, bitwise overloads would allow for this to be implemented fairly easily for a given enum. (Again, a smarter implementation is likely possible with actual effort.) enum Perm { case Read; case Write; case Exec; } class Permissions { private array $cases = []; public function __construct(Perm ...$perms) { foreach ($perms as $case) { $this->cases[$case->name] = 1; } } operator +(Perm $other, OperandPosition $pos): Permissions { $new = clone($this); $new->cases[$other->name] = 1; return $new; } operator +(Perm $other, OperandPosition $pos): Permissions { $new = clone($this); unset($new->cases[$other->name]); return $new; } operator |(Permissions $other, OperandPosition $pos): Permissions { $new = clone($this); foreach ($other->cases as $caseName => $v) { $new->cases[$caseName] = 1; } return $new; } operator &(Permission $other, OperandPosition $pos): Permissions { $new = new self(); $new->cases = array_key_intersect($this->cases, $other->cases); return $new; } // Not sure what operator makes sense here, so punting as this is just an example. public function has(Perm $p): bool { return array_key_exists($this->cases, $p->name); } } $p = new Permissions(Perm::Read); $p2 = $p + Perm::Exec; $p3 = $p2 | new Permissions(Perm::Write); $p3 -= Perm::Exec; $p3->has(Perm::Read); ## Function composition I have long argued that PHP needs both a pipe operator and a function composition operator. It wouldn't be ideal, but something like this is possible. (Ideally we'd use the string concat operator here, but the RFC doesn't show it. It would be a trivial change to use instead.) class Composed { /** @var \Closure[] */ private array $steps = []; public function __construct(?\Closure $c = null) { $this->steps[] = $c; } private static function fromList(array $cs): self { $new = new self(); $new->steps = $cs; return $new; } public function __invoke(mixed $arg): mixed { foreach ($this->steps as $step) { $arg = $step($arg); } return $arg; } operator +(\Closure $other, OperandPosition $pos): self { return match ($pos) { OperandPosition::LeftSide => self::fromArray([...$this->steps, $other]), OperandPosition::RightSide => self::fromArray([$other, ...$this->steps]), }; } } $fun = new Composed() + someFunc(...) + $obj->someMethod(...) + fn(string $a) => $a . ' (archived)' + strlen(...); $fun($input); // Calls each closure in turn. Note that there are a half-dozen libraries in the wild that do something akin to this, just much more clumsily, including in Laravel. The code above would be vastly simpler and easier to maintain and debug. ## Units Others have mentioned this before, but to make clear what it could look like: abstract readonly class MetricDistance implements MetricDistance { protected int $factor = 1; public function __construct(private int $length) {} public function +(MetricDistance $other, OperandPos $pos): self { return new self(floor(($this->length * $this->factor + $other->length * $other->factor)/$this->factor)); } public function -(MetricDistance $other, OperandPos $pos): self { return match ($pos) { OperandPosition::LeftSide => new self(floor(($this->length * $this->factor - $other->length * $other->factor)/$this->factor)), OperandPosition::RightSide => new self(floor($other->length * $other->factor - $this->length * $this->factor)/$this->factor)), }; } public function __toString(): string { return $this->length; } } readonly class Meters extends MetricDistance { protected int $factor = 1; } readonly class Kilometers extends MetricDistance { protected int $factor = 1000; } $m1 = new Meters(500); $k1 = new Kilometers(3); $m1 += $k1; print $m1; // prints 3500 $m1 + 12; // Error. 12 what? There's likely a bug in the above somewhere, but it's late and it still gets the point across for now. (Side note: The previous RFC supported abstract operator declarations, but not declarations on interfaces. That seems necessary for completeness.) ## Date and time DateTimeImmutable and DateInterval already do this, and they're not "fancy math." I consider all of the above to be reasonable, viable, and useful applications of operator overloading, none of which are fancy or esoteric math cases. Others may dislike them, stylistically. That's a subjective question, so opinions can differ. But the viability of the above cases is not disputable, so the claim that operator overloading is too niche to be worth it is, I would argue, demonstrably false. --Larry Garfield