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