Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:127683 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 1F23A1A00BC for ; Mon, 16 Jun 2025 15:19:08 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1750087028; bh=TlJclAmKV8PKSyP8N4a453NPJvVPop5qF5pfTnK1xEo=; h=From:Date:Subject:To:From; b=KnG0mlT94D4klk/8MqKS3aj7N7tKWKY7UxUFvlRVV5+qR4FcYa5fX9kDk7kUQ9VGx XhcKxA4GF3wGFjdk3npyo+5kyt4SOduV3+K5MV1v/RXS2r9j7lhi/2heqzSfOBiuhz lJdRjOVHgiz8a8WlzPP65iQRG7P7+i0NUR6OfBx3qsQTaeL9qNcBQTvPISAu+ZzsME XEj26OdEVHZ18zhcKsucdWgR1TvhT+HdyGjKW/g+x1bwtG3uJoTCHki9TgPFBqEh/j vEWdCKdGDLDVy26P2LLUPLkqyu94wdKu1hMgKDzf606+e+7P5vTlXSBtOcQpRwBWS3 7Wru02rFXod3A== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id EEAA6180051 for ; Mon, 16 Jun 2025 15:17:07 +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.2 required=5.0 tests=BAYES_20,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,DMARC_PASS,FREEMAIL_FROM, HTML_MESSAGE,RCVD_IN_DNSWL_NONE,RCVD_IN_MSPIKE_H3,RCVD_IN_MSPIKE_WL, 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-f52.google.com (mail-ej1-f52.google.com [209.85.218.52]) (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 15:16:57 +0000 (UTC) Received: by mail-ej1-f52.google.com with SMTP id a640c23a62f3a-ade5a0442dfso874833966b.1 for ; Mon, 16 Jun 2025 08:18:56 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1750087135; x=1750691935; darn=lists.php.net; h=to:subject:message-id:date:from:mime-version:from:to:cc:subject :date:message-id:reply-to; bh=m/3NJYfsi+K8/ckKOmLMM4dRSY2/+lhbL1/lqVjMo8o=; b=QKLc5ouC6EDcaKm5U85peKT3j59kr2Qk2CvwgHQEhgeNcoQQW+zk7/XfhVmXq9O0KT Qhe5RFU2vfzBJBn99bd6K2DRDkn1IEVSkm0DSkLOJ1zKMnOZZEKb1d5vvkXYupkFdNIN R3/xMYk6ADs0hRRqju+lFmWYff+WsiQWjiee+2tZ5AZLGuzOvObbx61vIlrwqUmNuJaE a6JSqIjmiFpAxk00kG4bxuZDNGSZI1I9v1ogVjL057VUAISn3/Th9F90PxAp8525GCsS 5HzMQlnEC422eGv7ILpJMifKK90T6/5YjgQAAJHNxfsPMoXxpYpfLwxXWnK3bPRGWDZc Aw/g== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1750087135; x=1750691935; h=to:subject:message-id:date:from:mime-version:x-gm-message-state :from:to:cc:subject:date:message-id:reply-to; bh=m/3NJYfsi+K8/ckKOmLMM4dRSY2/+lhbL1/lqVjMo8o=; b=N7TVr1e++h1G1Lnc+6lU6KEn2yIXwtqWec2rqXnWc3vYNJ2V77dIth/oKsioctkHYG l+Og2nzXMG2YCRx9HOn4x/nI+sCbvBe+nmvIP5w0YTpk0QCvjlwFJDNzdQeyS76oo4ku bGqJWzvR/A1a1Ee9Y1ZS9AvDyLlrjl9+kon1HwFhckpqOTtngspoHN3vscJDUvyZUuNF k/fnKEgOz0fA1Yy7Cv8r6cTWEJMuSqnE4p35DTvYWjQ7pv5MM9xH92qln0uIjijOnPz4 nCXs+pDnm1UJZK1IhSJcNRgGCBIBGIqjDX78i/I0mEmnERRyXxSptUDLCpsQg1mmwx/e UE1g== X-Gm-Message-State: AOJu0Yy2dT2b0o6rf07Zcm5DtrBooukPvjAWwTDsGVKELA02Tpea7zjm ltNRWIjJT0c1OVulIF7PZVAyR7DunLKIKaOZBAbbbHwH2YrR2dBnqJQEpVq9l0+n7CIv/c2b29+ /Cd+suJICzwNDg0uEuvdVtEj4qqRVj80nl5Rg X-Gm-Gg: ASbGncu+vRI9Kv2BmmGqUvbqQFF1T9VVh0Fdnm8Cla1e6AMweb7d/aJ7v+mfFGmMu6y iWo8JLmDinzRHAVvpHdORvJbwWGJIPqLWQbM8A9ZCAxGyM85o6f1nI9lz/HKPPUb5BHbaGAreE4 FRc9zdnIbOOlotRFWYQs394tPaePl+a2VHW61U5CoSU9lB X-Google-Smtp-Source: AGHT+IEZ38AeJJx0rC/t1y30rk54pzup3NSAPFv5f65H0cnnNbSuXemKoWXHTq6nUDyF3oPsK23Gqw7obXn54k1rlSM= X-Received: by 2002:a17:907:980f:b0:ad8:9909:20a3 with SMTP id a640c23a62f3a-adfad59953dmr997958966b.43.1750087135242; Mon, 16 Jun 2025 08:18:55 -0700 (PDT) Precedence: bulk list-help: list-post: List-Id: internals.lists.php.net x-ms-reactions: disallow MIME-Version: 1.0 Date: Mon, 16 Jun 2025 17:18:43 +0200 X-Gm-Features: AX0GCFvx-AxvJpNudr3kwdkC0SrXpMMRwpgeHgE3EG0sLu5AU8JThNYT6UqR-z0 Message-ID: Subject: [PHP-DEV] How hard would it be to add a "superyield" keyword? To: PHP internals Content-Type: multipart/alternative; boundary="00000000000005b02d0637b1ea9d" From: olleharstedt@gmail.com (=?UTF-8?Q?Olle_H=C3=A4rstedt?=) --00000000000005b02d0637b1ea9d Content-Type: text/plain; charset="UTF-8" Hello Internals, I was pondering a little about effect handlers today, and how they could work as a replacement for dependency injection and mocking. Let me show an example: query = $query; } } class Plugin { /* The "normal" way to do testing, by injecting the db object. Not needed here. public function __construct(Db $db) { $this->db = $db; } */ public function populateCreditCardData(&$receipt) { foreach ($receipt['items'] as &$item) { // 2 = credit card if ($item['payment_type'] == 2) { $query = $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 = $this->db->search($query->sql(), $query->params()); // Generator way, push the side-effect up the stacktrace using generators. $result = yield new QueryEffect($query); if ($result) { $item['card_product_name'] = $result[0]['card_product_name']; } } } } } // Dummy receipt $receipt = [ 'items' => [ [ 'payment_type' => 2 ] ] ]; $p = new Plugin(); // Database is not injected $gen = $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 = [ [ 'card_product_name' => '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, you 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 --- [1] - https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/ [2] - https://ocaml.org/manual/5.3/effects.html --00000000000005b02d0637b1ea9d Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable
Hello Internals,

I was pondering a little about eff= ect handlers today, and how they could
work as a replacement for depende= ncy injection and mocking. Let me show an
example:

<?php
require_once("vendor/autoload.php");

use Latitude\QueryB= uilder\Engine\MySqlEngine;
use Latitude\QueryBuilder\QueryFactory;
us= e function Latitude\QueryBuilder\field;

// Dummy db = connection
class Db
{
=C2=A0 =C2=A0 public function getQueryBuil= der()
=C2=A0 =C2=A0 {
=C2=A0 =C2=A0 =C2=A0 =C2=A0 return new QueryFac= tory(new MySqlEngine());
=C2=A0 =C2=A0 }
}

interface Effect {}=

class QueryEffect implements Effect
{
=C2=A0 =C2=A0 public $q= uery;

=C2=A0 =C2=A0 public function __construct($query)
=C2=A0 = =C2=A0 {
=C2=A0 =C2=A0 =C2=A0 =C2=A0 $this->query =3D $query;
=C2= =A0 =C2=A0 }
}

class Plugin
{
=C2=A0 =C2=A0 /* The "no= rmal" way to do testing, by injecting the db object. Not
needed her= e.
=C2=A0 =C2=A0 public function __construct(Db $db)
=C2=A0 =C2=A0 {<= br>=C2=A0 =C2=A0 =C2=A0 =C2=A0 $this->db =3D $db;
=C2=A0 =C2=A0 }
= =C2=A0 =C2=A0 */

=C2=A0 =C2=A0 public function populateCreditCardDat= a(&$receipt)
=C2=A0 =C2=A0 {
=C2=A0 =C2=A0 =C2=A0 =C2=A0 foreach = ($receipt['items'] as &$item) {
=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 if ($item['payment_type'] =3D=3D 2) {
=C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 $query =3D $this->db->getQuery= Builder()
=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 ->from('card_transa= ctions')
=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_transactio= n_id']))
=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 // Normal way: Call the injected dependency class directly.<= br>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 //$result =3D $t= his->db->search($query->sql(),
$query->params());

=C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 // Generator 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 $result =3D yield new QueryEffect= ($query);
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if ($r= esult) {
=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_produ= ct_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 }
}

// Dummy receipt
$receipt =3D [
= =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 'payment_type' =3D> 2=
=C2=A0 =C2=A0 =C2=A0 =C2=A0 ]
=C2=A0 =C2=A0 ]
];
$p =3D new Pl= ugin(); =C2=A0// Database is not injected
$gen =3D $p->populateCredit= CardData($receipt);
foreach ($gen as $effect) {
=C2=A0 =C2=A0 // Call= $db here instead of injecting it.
=C2=A0 =C2=A0 // But now I have to pr= opagate the $gen logic all over the call stack,
with "yield from&qu= ot;? :(
=C2=A0 =C2=A0 // Effect handlers solve this by forcing an effect= up in the stack
trace similar to exceptions.

<= div>=C2=A0=C2=A0=C2=A0 // Dummy db result
=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= 'card_product_name' =3D> 'KLARNA',
=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.
p= rint_r($receipt);

---

OK, so the problem with above code is t= hat, in order for it to work, you
have to add "yield from" fro= m the top to the bottom of the call stack,
polluting the code-base simil= ar to what happens with "async" in JavaScript.
Also see the &q= uot;Which color is your function" article [1].

For this design = pattern to work seamlessly, there need to be a way to yield
"all th= e way", so to speak, similar to what an exception does, and how
eff= ect 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 concept= ually too different from generators?
Would it be easier to add a way to = "jump back" from a catched exception
(kinda abusing the except= ion use-case, but that's how effect handlers work,
more or less)?
Thanks for reading :)

Olle

---

= [1] -
https://journal.stuffwithstuff.com/2015/02/01/what-col= or-is-your-function/

[2] - https://ocaml.org/manual/5.3/effects.html --00000000000005b02d0637b1ea9d--