Den mån 16 juni 2025 kl 20:11 skrev Larry Garfield <la...@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. :-) 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. ;) Anyway, this topic can continue somewhere else. Thanks for the feedback! Olle [1] - https://koka-lang.github.io/koka/doc/book.html#why-effects