[PHP-DEV] How hard would it be to add a "superyield" keyword?

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:

<?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 { public function getQueryBuilder() { return new QueryFactory(new MySqlEngine()); } } interface Effect {} class QueryEffect implements Effect { public $query; public function __construct($query) { $this->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/](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/) [2] - [https://ocaml.org/manual/5.3/effects.html](https://ocaml.org/manual/5.3/effects.html)

On Mon, Jun 16, 2025, at 17:18, Olle Härstedt wrote:

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:

<?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 { public function getQueryBuilder() { return new QueryFactory(new MySqlEngine()); } } interface Effect {} class QueryEffect implements Effect { public $query; public function __construct($query) { $this->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/](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/) [2] - [https://ocaml.org/manual/5.3/effects.html](https://ocaml.org/manual/5.3/effects.html)

You want to jump out of the current stack frame to one a higher one, and possibly resume execution?

The best way to do that is to use fibers. They basically do exactly this behavior.

— Rob

Hi,

Great tip, thank you, I had forgot about those! Will try.

Olle

Den mån 16 juni 2025 kl 17:32 skrev Rob Landers rob@bottled.codes:

On Mon, Jun 16, 2025, at 17:18, Olle Härstedt wrote:

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:

<?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 { public function getQueryBuilder() { return new QueryFactory(new MySqlEngine()); } } interface Effect {} class QueryEffect implements Effect { public $query; public function __construct($query) { $this->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/](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/) [2] - [https://ocaml.org/manual/5.3/effects.html](https://ocaml.org/manual/5.3/effects.html)

You want to jump out of the current stack frame to one a higher one, and possibly resume execution?

The best way to do that is to use fibers. They basically do exactly this behavior.

— Rob

On Mon, Jun 16, 2025, at 10:18 AM, Olle Härstedt wrote:

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:

<?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
{
    public function getQueryBuilder()
    {
        return new QueryFactory(new MySqlEngine());
    }
}

interface Effect {}

class QueryEffect implements Effect
{
    public $query;

    public function __construct($query)
    {
        $this->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"? :frowning:
    // 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 :slight_smile:

Olle

Algebraic effects is a... big and interesting topic. :slight_smile: If we were to go that route, though, I would want to see something more formal than just a "yield far." That's basically another kind of unchecked exception, whereas I want us to move more toward checked exceptions.

--Larry Garfield

Den mån 16 juni 2025 kl 20:11 skrev Larry Garfield <larry@garfieldtech.com>:

On Mon, Jun 16, 2025, at 10:18 AM, Olle Härstedt wrote:

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:

<?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 { public function getQueryBuilder() { return new QueryFactory(new MySqlEngine()); } } interface Effect {} class QueryEffect implements Effect { public $query; public function __construct($query) { $this->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

Algebraic effects is a… big and interesting topic. :slight_smile: If we were to go that route, though, I would want to see something more formal than just a “yield far.” That’s basically another kind of unchecked exception, whereas 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 = 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. :wink:

Anyway, this topic can continue somewhere else. Thanks for the feedback!

Olle

[1] - https://koka-lang.github.io/koka/doc/book.html#why-effects

On Mon, Jun 16, 2025, at 20:10, Larry Garfield wrote:

On Mon, Jun 16, 2025, at 10:18 AM, Olle Härstedt wrote:

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:

<?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 { public function getQueryBuilder() { return new QueryFactory(new MySqlEngine()); } } interface Effect {} class QueryEffect implements Effect { public $query; public function __construct($query) { $this->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

Algebraic effects is a… big and interesting topic. :slight_smile: If we were to go that route, though, I would want to see something more formal than just a “yield far.” That’s basically another kind of unchecked exception, whereas I want us to move more toward checked exceptions.

–Larry Garfield

I think this might be entirely possible via an extension… there might need to be some hooks added to php-src but nothing that would require an RFC.

— Rob