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/ [2] - https://ocaml.org/manual/5.3/effects.html