Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:127691 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 lists.php.net (Postfix) with ESMTPS id 86A0F1A00BC for ; Mon, 16 Jun 2025 18:28:39 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1750098400; bh=CCIq+9E8NCzIel94p6GqEI2srqBYfP15+VsYfDeMqlY=; h=References:In-Reply-To:From:Date:Subject:To:Cc:From; b=QSjQEV1s/6m625L7uHw/hYT6YkdXeK7Ps1WwXRkxnaIOso3J2u8fFI9gpDV5PpnWn PBbt1tZ+7L+573mYfmpDOcfhrBGdfUBp/4UqAWseAe/q7Uj0NJj2laFZyiIFyR0efl U9zcx5Hz3WKsdo9jwlzKlopOuWlQ/MsJIT10acrgF2+CBnuVbpbkpq5NOjgCVEtoiz +nj1gHH/Skgm4glxvGbmIs19waKLWU3+R+QB10IQ7uEvgKA4r1pC+9rwtLl+3p/KCn rmpx73oEzWTcxl4Z/FYOhBK7qF47dkQ48LuBu0ilb4AK9NMV27e6hRrNqNLRnRw5Lc WXHRr3CgEe1zQ== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id 24F311801D9 for ; Mon, 16 Jun 2025 18:26:39 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 4.0.1 (2024-03-25) on php-smtp4.php.net X-Spam-Level: X-Spam-Status: No, score=-0.4 required=5.0 tests=BAYES_50,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,DMARC_PASS,FREEMAIL_FROM, HTML_MESSAGE,RCVD_IN_DNSWL_NONE,RCVD_IN_MSPIKE_H2,SPF_HELO_NONE, SPF_PASS autolearn=no autolearn_force=no version=4.0.1 X-Spam-Virus: Error (Cannot connect to unix socket '/var/run/clamav/clamd.ctl': connect: Connection refused) X-Envelope-From: Received: from mail-ej1-f48.google.com (mail-ej1-f48.google.com [209.85.218.48]) (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 ; Mon, 16 Jun 2025 18:26:38 +0000 (UTC) Received: by mail-ej1-f48.google.com with SMTP id a640c23a62f3a-ad88d77314bso1074727166b.1 for ; Mon, 16 Jun 2025 11:28:37 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1750098516; x=1750703316; darn=lists.php.net; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:from:to:cc:subject:date:message-id:reply-to; bh=SBoqkSTBGf7Srj0RMsCMET5POmdCWsn7f//HPtSFbTI=; b=ZsMgN1Acg5DstctqcSNIHOllhFD8LGme2VgEa3doZ3CKCouaVKd8fgn1+uHH2mnxcG L2+iqjYLNRI08sviF8XLaKo/CYJbrRKOqmpROEq+EJb/ESS1yqRTnr2alCQJY2xd25Tr FQ5OrM3wQlP4STBldq5t5sPgD5yjkFXV5nu46RDpOqSoEQ4bamh8H1+N+XnoFfLyb4DI M5tadud7oRhsMyEqgL98rSEaIkXya3R7QMDVUAjuh70uPewgdn8mEyNXLyI9X48RHHcy J5X8mfxO8pYLAohnsoRBBayC7ZVSBsJajcTIG/1ATqhQK/7Y8uQg3+YH/+7MDyAW0nA9 LLmg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1750098516; x=1750703316; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:x-gm-message-state:from:to:cc:subject:date:message-id :reply-to; bh=SBoqkSTBGf7Srj0RMsCMET5POmdCWsn7f//HPtSFbTI=; b=i9peEQu3WhipiLsTGuZFrw3GHmfqI5gy0X2rKNyhlpxrp2Q0e2FOpMSv/sBCar3kEv yXujhncMxTaEUBcOe3a5cqTSl2na4C5m2pizqLAMEYi7eQe6Yb0DZlRke/NXOe8ySAvZ GLMxKj9n0DxWC5hQ3+TJ/8b5i7BpsZwkq0ZBG/mM+wCg/VeWOcsMv+sMb1AO3MnPS6Tp XwIWhUNtfS2vNtgDHLoRU04ONBnogeunOvekaXMND6XF4zB7swzV6O4/e6bngNoTQt9S qM+RKlHpbfu/LqgcBNdDeVDAFW1uDjeUzYAVN79HaQEoiFr/1PRCaZQkR8s0zB4zi6gU Huiw== X-Gm-Message-State: AOJu0Yxd1qelQdbwojE/6V6QBo7J7g+70VUEbI1uy7aO8lfX+akuif1W xcY7mSAuqcs2izVi6w5AFAsZHu5AbuM2KThNo58XnkxU4BgjZYCGERWwgH2cMLlXIUbQj1SW0cm JxO7ATyXHWsDWFSHhyiwielNXbA2aczo= X-Gm-Gg: ASbGnctFgs+nSqKUzHTr0GoOVH5hXMa8nUqbOPE4bWNPGHT59O7At77m1dEcoe7dHJN 3293T3bDIh3KR+b7j5M7o8u8NLAVL+y+mMpZxBtUG7K5MBeXjlFtAMGfgKRnwZS2sFdLpsMxZTo nwRvIcZXwHuq6oUTWKp4Y+AS4+fuLdo1CVKKpQk4jaHn4= X-Google-Smtp-Source: AGHT+IFegzKRAu924PhGaNvSZrY+X0K34340Lav8lE8F6rnaetR/QZZjeQIUNE9ZtNWJ52fqdpHg3SITy6lEJn6y4DE= X-Received: by 2002:a17:907:9809:b0:ad8:a4a8:102c with SMTP id a640c23a62f3a-adfad368010mr964616166b.11.1750098516289; Mon, 16 Jun 2025 11:28:36 -0700 (PDT) Precedence: bulk list-help: list-post: List-Id: internals.lists.php.net x-ms-reactions: disallow MIME-Version: 1.0 References: In-Reply-To: Date: Mon, 16 Jun 2025 20:28:24 +0200 X-Gm-Features: AX0GCFvW8InDQIDg22fYgeb6Hyw1HCyhzL7D9TzzVAiVhCqQoCwKWQeT0dt6rf4 Message-ID: Subject: Re: [PHP-DEV] How hard would it be to add a "superyield" keyword? To: Larry Garfield Cc: php internals Content-Type: multipart/alternative; boundary="00000000000062ad6f0637b4906e" From: olleharstedt@gmail.com (=?UTF-8?Q?Olle_H=C3=A4rstedt?=) --00000000000062ad6f0637b4906e Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable Den m=C3=A5n 16 juni 2025 kl 20:11 skrev Larry Garfield : > On Mon, Jun 16, 2025, at 10:18 AM, Olle H=C3=A4rstedt wrote: > > Hello Internals, > > > > I was pondering a little about effect handlers today, and how they coul= d > > work as a replacement for dependency injection and mocking. Let me show > an > > example: > > > > > > > require_once("vendor/autoload.php"); > > > > use Latitude\QueryBuilder\Engine\MySqlEngine; > > use Latitude\QueryBuilder\QueryFactory; > > use function Latitude\QueryBuilder\field; > > > > // Dummy db connection > > class Db > > { > > public function getQueryBuilder() > > { > > return new QueryFactory(new MySqlEngine()); > > } > > } > > > > interface Effect {} > > > > class QueryEffect implements Effect > > { > > public $query; > > > > public function __construct($query) > > { > > $this->query =3D $query; > > } > > } > > > > class Plugin > > { > > /* The "normal" way to do testing, by injecting the db object. Not > > needed here. > > public function __construct(Db $db) > > { > > $this->db =3D $db; > > } > > */ > > > > public function populateCreditCardData(&$receipt) > > { > > foreach ($receipt['items'] as &$item) { > > // 2 =3D credit card > > if ($item['payment_type'] =3D=3D 2) { > > $query =3D $this->db->getQueryBuilder() > > ->select('card_product_name ') > > ->from('card_transactions') > > > ->where(field('id')->eq($item['card_transaction_id'])) > > ->compile(); > > > > // Normal way: Call the injected dependency class > directly. > > //$result =3D $this->db->search($query->sql(), > > $query->params()); > > > > // Generator way, push the side-effect up the stacktrac= e > > using generators. > > $result =3D yield new QueryEffect($query); > > if ($result) { > > $item['card_product_name'] =3D > > $result[0]['card_product_name']; > > } > > } > > } > > } > > } > > > > // Dummy receipt > > $receipt =3D [ > > 'items' =3D> [ > > [ > > 'payment_type' =3D> 2 > > ] > > ] > > ]; > > $p =3D new Plugin(); // Database is not injected > > $gen =3D $p->populateCreditCardData($receipt); > > foreach ($gen as $effect) { > > // Call $db here instead of injecting it. > > // But now I have to propagate the $gen logic all over the call > stack, > > with "yield from"? :( > > // Effect handlers solve this by forcing an effect up in the stack > > trace similar to exceptions. > > > > // Dummy db result > > $rows =3D [ > > [ > > 'card_product_name' =3D> 'KLARNA', > > ] > > ]; > > $gen->send($rows); > > } > > > > // Receipt item now has card_product_name populated properly. > > print_r($receipt); > > > > --- > > > > OK, so the problem with above code is that, in order for it to work, yo= u > > have to add "yield from" from the top to the bottom of the call stack, > > polluting the code-base similar to what happens with "async" in > JavaScript. > > Also see the "Which color is your function" article [1]. > > > > For this design pattern to work seamlessly, there need to be a way to > yield > > "all the way", so to speak, similar to what an exception does, and how > > effect handlers work in OCaml [2]. > > > > The question is, would this be easy, hard, or very hard to add to the > > current PHP source code? Is it conceptually too different from > generators? > > Would it be easier to add a way to "jump back" from a catched exception > > (kinda abusing the exception use-case, but that's how effect handlers > work, > > more or less)? > > > > Thanks for reading :) > > > > Olle > > Algebraic effects is a... big and interesting topic. :-) If we were to g= o > that route, though, I would want to see something more formal than just a > "yield far." That's basically another kind of unchecked exception, where= as > I want us to move more toward checked exceptions. > > --Larry Garfield > I agree, and I was surprised to see OCaml going towards untyped effect handlers, compared to, say, what they have in Koka [1]. I tried with Fiber::suspend(new QueryEffect($query)); and it works just fine, but the intentionality of the code is a bit weak. I guess one could just wrap it to make its purpose more clear, like function query($query) { return Fiber::suspend(new QueryEffect($query)); } // Inside fiber // Query building logic omitted... $rows =3D query($query); // Yield to top-level effect handler Commitment to this design pattern is pretty high, since it's not contained within a class or module. One could say the same about DI, perhaps. ;) Anyway, this topic can continue somewhere else. Thanks for the feedback! Olle [1] - https://koka-lang.github.io/koka/doc/book.html#why-effects --00000000000062ad6f0637b4906e Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable


Den m=C3=A5n 16= juni 2025 kl 20:11 skrev Larry Garfield <larry@garfieldtech.com>:
On Mon, Jun 16, 2025, at 10:18 AM, Olle H=C3=A4= rstedt wrote:
> Hello Internals,
>
> I was pondering a little about effect handlers today, and how they cou= ld
> work as a replacement for dependency injection and mocking. Let me sho= w an
> example:
>
> <?php
>
> require_once("vendor/autoload.php");
>
> use Latitude\QueryBuilder\Engine\MySqlEngine;
> use Latitude\QueryBuilder\QueryFactory;
> use function Latitude\QueryBuilder\field;
>
> // Dummy db connection
> class Db
> {
>=C2=A0 =C2=A0 =C2=A0public function getQueryBuilder()
>=C2=A0 =C2=A0 =C2=A0{
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0return new QueryFactory(new MySqlEngi= ne());
>=C2=A0 =C2=A0 =C2=A0}
> }
>
> interface Effect {}
>
> class QueryEffect implements Effect
> {
>=C2=A0 =C2=A0 =C2=A0public $query;
>
>=C2=A0 =C2=A0 =C2=A0public function __construct($query)
>=C2=A0 =C2=A0 =C2=A0{
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0$this->query =3D $query;
>=C2=A0 =C2=A0 =C2=A0}
> }
>
> class Plugin
> {
>=C2=A0 =C2=A0 =C2=A0/* The "normal" way to do testing, by inj= ecting the db object. Not
> needed here.
>=C2=A0 =C2=A0 =C2=A0public function __construct(Db $db)
>=C2=A0 =C2=A0 =C2=A0{
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0$this->db =3D $db;
>=C2=A0 =C2=A0 =C2=A0}
>=C2=A0 =C2=A0 =C2=A0*/
>
>=C2=A0 =C2=A0 =C2=A0public function populateCreditCardData(&$receip= t)
>=C2=A0 =C2=A0 =C2=A0{
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0foreach ($receipt['items'] as= &$item) {
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0// 2 =3D credit card >=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if ($item['payment_= type'] =3D=3D 2) {
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0$query = =3D $this->db->getQueryBuilder()
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0->select('card_product_name ')
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0->from('card_transactions')
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0->where(field('id')->eq($item['card_transaction_id&= #39;]))
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0->compile();
>
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0// Normal= way: Call the injected dependency class directly.
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0//$result= =3D $this->db->search($query->sql(),
> $query->params());
>
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0// Genera= tor way, push the side-effect up the stacktrace
> using generators.
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0$result = =3D yield new QueryEffect($query);
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0if ($resu= lt) {
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0$item['card_product_name'] =3D
> $result[0]['card_product_name'];
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0}
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0}
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0}
>=C2=A0 =C2=A0 =C2=A0}
> }
>
> // Dummy receipt
> $receipt =3D [
>=C2=A0 =C2=A0 =C2=A0'items' =3D> [
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0[
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0'payment_type' = =3D> 2
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0]
>=C2=A0 =C2=A0 =C2=A0]
> ];
> $p =3D new Plugin();=C2=A0 // Database is not injected
> $gen =3D $p->populateCreditCardData($receipt);
> foreach ($gen as $effect) {
>=C2=A0 =C2=A0 =C2=A0// Call $db here instead of injecting it.
>=C2=A0 =C2=A0 =C2=A0// But now I have to propagate the $gen logic all o= ver the call stack,
> with "yield from"? :(
>=C2=A0 =C2=A0 =C2=A0// Effect handlers solve this by forcing an effect = up in the stack
> trace similar to exceptions.
>
>=C2=A0 =C2=A0 =C2=A0// Dummy db result
>=C2=A0 =C2=A0 =C2=A0$rows =3D [
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0[
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0'card_product_name&= #39; =3D> 'KLARNA',
>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0]
>=C2=A0 =C2=A0 =C2=A0];
>=C2=A0 =C2=A0 =C2=A0$gen->send($rows);
> }
>
> // Receipt item now has card_product_name populated properly.
> print_r($receipt);
>
> ---
>
> OK, so the problem with above code is that, in order for it to work, y= ou
> have to add "yield from" from the top to the bottom of the c= all stack,
> polluting the code-base similar to what happens with "async"= in JavaScript.
> Also see the "Which color is your function" article [1].
>
> For this design pattern to work seamlessly, there need to be a way to = yield
> "all the way", so to speak, similar to what an exception doe= s, and how
> effect handlers work in OCaml [2].
>
> The question is, would this be easy, hard, or very hard to add to the<= br> > current PHP source code? Is it conceptually too different from generat= ors?
> Would it be easier to add a way to "jump back" from a catche= d exception
> (kinda abusing the exception use-case, but that's how effect handl= ers work,
> more or less)?
>
> Thanks for reading :)
>
> Olle

Algebraic effects is a... big and interesting topic. :-)=C2=A0 If we were t= o go that route, though, I would want to see something more formal than jus= t a "yield far."=C2=A0 That's basically another kind of unche= cked exception, whereas I want us to move more toward checked exceptions.
--Larry Garfield

I agree, and I was sur= prised to see OCaml going towards untyped effect handlers, compared to, say= , what they have in Koka [1].

I tried with Fiber::= suspend(new QueryEffect($query)); and it works just fine, but the intention= ality of the code is a bit weak. I guess one could just wrap it to make its= purpose more clear, like

function query($query)
{
=C2=A0 return Fiber::suspend(new QueryEffect($query));=
}

// Inside fiber
// Query bu= ilding logic omitted...
$rows =3D query($query);=C2=A0 // Yield t= o top-level effect handler

Commitment to this desi= gn pattern is pretty high, since it's not contained within a class or m= odule. One could say the same about DI, perhaps. ;)

Anyway, this topic can continue somewhere else. Thanks for the feedback!<= /div>

Olle

=C2=A0
<= /div>
--00000000000062ad6f0637b4906e--