Den mån 16 juni 2025 kl 20:11 skrev Larry Garfield <[email protected]>:
> 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