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. :-)  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

Reply via email to