Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:125615 X-Original-To: internals@lists.php.net Delivered-To: internals@lists.php.net Received: from php-smtp4.php.net (php-smtp4.php.net [45.112.84.5]) by qa.php.net (Postfix) with ESMTPS id 1E8AD1A00BD for ; Wed, 18 Sep 2024 03:31:51 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1726630436; bh=0JWohk0o/b+KV9W1PY2mHj0TsV3qjnA/rL+ccMG6z1o=; h=Date:From:To:In-Reply-To:References:Subject:From; b=cqVtOHcU/8wCPpNjFACXd5hO6VQAaHyOSBprL0NK9b7g+1u98PaKp6Jy5CqAL+2kY 0jFYIggZtGM1jIq4v9z9NnbjdQ09Se6KYQX4NCZlaCse5hG4/QwSBANxGpALtQ4+qV kVH57gF2w17sYu2lv5ZO/cVlZPlDZAVfZLDW6TNfsze6pFut1Mf4LP+G7mpsdI/7sv DQb6SXW2sCrrpouGry6HzSxr7wG2Iw/PhbkvvmqrbsNfCazqvjaT6yc3S3/PyoMn/j UohR6OYkyYLmVjMLnIMqf5uUtaWfdrL8H1fjdU5sDB/O+xI18Wh896ZtdMxks1SSnH 9KL5rDsA3UCmA== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id 27C30180055 for ; Wed, 18 Sep 2024 03:33:55 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 4.0.0 (2022-12-13) on php-smtp4.php.net X-Spam-Level: X-Spam-Status: No, score=-0.1 required=5.0 tests=BAYES_50,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,DMARC_MISSING,RCVD_IN_DNSWL_LOW, SPF_HELO_PASS,SPF_NONE autolearn=no autolearn_force=no version=4.0.0 X-Spam-Virus: No X-Envelope-From: Received: from fhigh2-smtp.messagingengine.com (fhigh2-smtp.messagingengine.com [103.168.172.153]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by php-smtp4.php.net (Postfix) with ESMTPS for ; Wed, 18 Sep 2024 03:33:54 +0000 (UTC) Received: from phl-compute-01.internal (phl-compute-01.phl.internal [10.202.2.41]) by mailfhigh.phl.internal (Postfix) with ESMTP id 0F97B11401E5 for ; Tue, 17 Sep 2024 23:31:49 -0400 (EDT) Received: from phl-imap-06 ([10.202.2.83]) by phl-compute-01.internal (MEProxy); Tue, 17 Sep 2024 23:31:49 -0400 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= garfieldtech.com; h=cc:content-transfer-encoding:content-type :content-type:date:date:from:from:in-reply-to:in-reply-to :message-id:mime-version:references:reply-to:subject:subject:to :to; s=fm1; t=1726630309; x=1726716709; bh=0PS6WBo0L+o2Nx7mr7CNo XzLs8dGvnKHfIGpWvjKAHg=; b=AwhzKsUusbCASCwt4srC4pq7Kuh9gos6Xxy5S 8863N+JQA4sndnneL+t3yrPKBO0SkS9IYRU8ixb2nDV7MJvoGQ7Kd5JhMQ5V3UE0 3yvMGuv0mN+sMlzDJjAlYVGnDWn1E6r3z2umV6Y36ahSNDI152k0kFPO3kM694wX wf7j4PnSc4weVjElcatuXkDB/zvFU+L7hIIxhKHSF0X+x76jhXc1bVCga6nPnnsI RaS3Y6wbxA89Ql/kgUKWatB+YYpXCmqWKWPKxLMaFkMWqn1bklTmPVtdgx656FQb N13gkxPYP2Y0anF4njnpdgb3hnQIoN73I8dk65dDDbWmsO0Xg== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:content-transfer-encoding:content-type :content-type:date:date:feedback-id:feedback-id:from:from :in-reply-to:in-reply-to:message-id:mime-version:references :reply-to:subject:subject:to:to:x-me-proxy:x-me-proxy :x-me-sender:x-me-sender:x-sasl-enc; s=fm1; t=1726630309; x= 1726716709; bh=0PS6WBo0L+o2Nx7mr7CNoXzLs8dGvnKHfIGpWvjKAHg=; b=A K7ddgq7XMSAcjqS7smWtCmBpxW4ghb9pyHOuvx/amu1uzvawbbRyUitX3CfVlOev kmBYHei0JSAR7xC4ol+33E2Bn7nrZ2J0VBCXOGJAAHUYpGoskX15iE/rW4UwQK2E 0s5+653TriRktMB3Xl+Cbcn24GE51JVVuykY/bZV2c1PhWOtg0nnn9eajzEGB10q H9W1bWw7U2aH0XFCzCDnJtT4/FifnY6iW4XDeL/Ji/FAOSc/3RHPQ8C1OjU/Kufw TwmjyvOkrrJM6IdmTQ9gnrcinS0RX43Krv3carsJADBz/zWXWn1tcXXQ7M9AHI7F IQB1xK3nT484AF0lFHSCg== X-ME-Sender: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgeeftddrudekkedgjeefucetufdoteggodetrfdotf fvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdggtfgfnhhsuhgsshgtrhhisggvpdfu rfetoffkrfgpnffqhgenuceurghilhhouhhtmecufedttdenucesvcftvggtihhpihgvnh htshculddquddttddmnecujfgurhepofggfffhvffkjghfufgtgfesthejredtredttden ucfhrhhomhepfdfnrghrrhihucfirghrfhhivghlugdfuceolhgrrhhrhiesghgrrhhfih gvlhguthgvtghhrdgtohhmqeenucggtffrrghtthgvrhhnpeeuheelgeekveeludetieet vefftdelhffgvdetjedvuedtfefghfdtteehkedvteenucffohhmrghinhepphhhphdrnh gvthdpghhithhhuhgsrdgtohhmpdhrvghsvggrrhgthhdqnhhothgvshdrmhgunecuvehl uhhsthgvrhfuihiivgeptdenucfrrghrrghmpehmrghilhhfrhhomheplhgrrhhrhiesgh grrhhfihgvlhguthgvtghhrdgtohhmpdhnsggprhgtphhtthhopedupdhmohguvgepshhm thhpohhuthdprhgtphhtthhopehinhhtvghrnhgrlhhssehlihhsthhsrdhphhhprdhnvg ht X-ME-Proxy: Feedback-ID: i8414410d:Fastmail Received: by mailuser.phl.internal (Postfix, from userid 501) id B04C429C006F; Tue, 17 Sep 2024 23:31:48 -0400 (EDT) X-Mailer: MessagingEngine.com Webmail Interface Precedence: bulk list-help: list-post: List-Id: internals.lists.php.net x-ms-reactions: disallow MIME-Version: 1.0 Date: Tue, 17 Sep 2024 22:31:03 -0500 To: "php internals" Message-ID: <070c5592-6e73-44aa-878f-0331935daec7@app.fastmail.com> In-Reply-To: References: Subject: Re: [PHP-DEV] [Pre-RFC Discussion] User Defined Operator Overloads (again) Content-Type: text/plain Content-Transfer-Encoding: 7bit From: larry@garfieldtech.com ("Larry Garfield") 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