Using PHP Generators for Control Flow
As you may know from my last post covering Ember, I have become very interested in expanding my javascript skill set. I posted my 10 part Ember video series to /r/javascript and was glad to see it get some upvotes. I noticed a post about Koa.js sitting next to mine. Koa is touted as a next generation framework for node from the Express team. I have heard of Express but haven't done anything with it. Upon seeing there was something even newer, I was intrigued and decided to check it out.
What the heck is this voodoo?
I was really taken aback when I came to the Cascading section. The example showed a asterisk right in the middle of a closure definition and the use of a yield
keyword.
// copy/pasted directly from Koa's docs
var koa = require('koa');
var app = koa();
// x-response-time
app.use(function *(next){
var start = new Date;
yield next;
var ms = new Date - start;
this.set('X-Response-Time', ms + 'ms');
});
// logger
app.use(function *(next){
var start = new Date;
yield next;
var ms = new Date - start;
console.log('%s %s - %s', this.method, this.url, ms);
});
// response
app.use(function *(){
this.body = 'Hello World';
});
app.listen(3000);
I had never seen that before. Upon some further investigation I learned that they are generators. Generators allow you to do some really crazy iterations. You can stop the iteration and give the calling code a value by using the yield
keyword. You can also send the generator data back in the middle of an iteration. They're pretty mind bottling at first glance. I was curious to know if php has generators and guess what, they're new in 5.5!
Anthony Ferrara has great article on generators here.
SitePoint also has a pretty good post about them here.
A Wikipedia article about generators.
Re-purposing generators
After I got a handle on what generators are I went back to the Koa website to review what they were doing. It didn't looking like they were using them for iteration though. It looks like they're using them to pass control to another generator, where execution halts in the first generator and continues in the next generator, which the first generator is completely unaware of. Once there are no more generators to continue on, flow unwinds back to the yield
line in each generator. Koa's using them for a sort of cascading middleware. Look at Koa's docs if what I said didn't make any sense.
I found this concept to be incredibly clever and interesting. I wonder if I could implement the same idea in php...
Mental Gymnastics
This turned out to be a really fun challenge. Man...programming has really warped my idea of fun. Surprisingly, I was able to achieve the same effect. It took me a little while to figure it out and I didn't cheat and look at Koa's source! I'm not even sure with my javascript skills it would've helped much anyway.
Right this way
Let me show you an example.
$obj = new \stdClass;
$obj->result = '';
$first = function() use ($obj) {
$obj->result .= ' 1 ';
yield ControlFlow::NEXT;
$obj->result .= ' 2 ';
};
$second = function() use ($obj){
$obj->result .= ' 3 ';
yield ControlFlow::NEXT;
$obj->result .= ' 4 ';
};
$third = function() use ($obj){
$obj->result .= ' 5 ';
return ControlFlow::NEXT;
};
$fourth = function() use ($obj){
$obj->result .= ' 6 ';
return ControlFlow::NEXT;
};
$flow = new ControlFlow;
$flow->queue($first()) //notice the parenthesis for generators
->queue($second())
->queue($third)
->queue($fourth);
$flow->run();
echo trim($obj->result); // '1 3 5 6 4 2'
My ControlFlow
object accepts two object types in the queue()
method, \Generator
's and \Closure
's. It will always execute each queued action when the run()
method is called but the order of execution is dependent on the contents of each generator/closure.
In the above example, the flow occurs like so:
- execution starts in the
$first
generator - ' 1 ' is appended to
$obj->result
- the
$first
generator then attempts to stop it's execution and yield/pass control to an unknown piece of code - the
ControlFlow
then passes control to the next generator/closure which happens to be$second
- ' 3 ' is appended
- the
$second
generator then attempts to stop it's execution and yield/pass control to an unknown piece of code - the
ControlFlow
then passes control to the next generator/closure which happens to be$third
- ' 5 ' is appended
- the
ControlFlow
then passes control to the next generator/closure which happens to be$fourth
- ' 6 ' is appended
- the
ControlFlow
attempts to pass control to the next generator/closure but none is found so it starts to unwind - control is then relinquished back to the last yield, which is in
$second
- ' 4 ' is appended
- control is then relinquished back to the previous yield, which is in
$first
- ' 2 ' is appended
- '1 3 5 6 4 2' is echoed
If the $third
closure did not return ControlFlow::NEXT
, execution would have gone like this(the difference starts on step 9):
- execution starts in the
$first
generator - ' 1 ' is appended
- the
$first
generator then attempts to stop it's execution and yield/pass control to an unknown piece of code - the
ControlFlow
then passes control to the next generator/closure which happens to be$second
- ' 3 ' is appended
- the
$second
generator then attempts to stop it's execution and yield/pass control to an unknown piece of code - the
ControlFlow
then passes control to the next generator/closure which happens to be$third
- ' 5 ' is appended
$third
doesn't direct flow to another piece of code, so it starts to unwind- control is then relinquished back to the last yield, which is in
$second
- ' 4 ' is appended
- control is then relinquished back to the previous yield, which is in
$first
- ' 2 ' is appended
- control is then passed to the next action it hasn't executed yet,
$fourth
- ' 6 ' is appended
- '1 3 5 4 2 6' is echoed
As you can see, each action will always be executed, but their order is entirely dependent on the contents of the actions.
It's worth mentioning, in the first scenario, that if there was a $fifth
generator action that looked like the others(append ' 7 ' , yield next, append ' 8 '), both ' 7 ' and ' 8 ' would be appended at the same time because it would be the last action in the queue. It would then start to unwind.
I showed my coworker my example and he likened it to using templates, which is a great analogy. You have a template that has a placeholder for other markup to be rendered into. That base template has no idea what's going to get rendered into to but as soon as that happens it can continue on and render the rest of its own markup. I'm applying this same principle to php code.
Is this useful?
I have no idea! It's a pretty neat trick though. It seems like it could be a really confusing way to do some sort of pub/sub event propagation considering that generators are not required to yield
anything. They must have the yield
keyword in them but it could be wrapped up in a an if-statement that may not allow the execution to get there. Generators are strange beasts.
I would love to see someone smarter than me take this idea and run with it. I would be very curious to see what you come up with!
ControlFlow
Here's my ControlFlow
class. It should probably be named something like YoYo
.
class ControlFlow
{
const NEXT = 'NEXT';
/**
* @var array
*/
protected $queued = [];
/**
* @var array
*/
protected $initiated = [];
/**
* Queue an action
*
* @param \Closure|\Generator $action
* @return $this
* @throws \InvalidArgumentException
*/
public function queue($action)
{
if ( ! $action instanceof \Closure && ! $action instanceof \Generator) {
throw new \InvalidArgumentException('Queued action must be a \Closure or \Generator');
}
$this->queued[] = $action;
return $this;
}
/**
* Run all queued actions
*
* @return void
*/
public function run()
{
foreach ($this->queued as $action) {
$this->runAction($action);
}
//flush everything because generators can't be reopened
$this->flush();
}
/**
* Run a queued action
*
* @param \Closure|\Generator $action
* @return void
*/
private function runAction($action)
{
if (in_array($action, $this->initiated, true)) return;
$this->initiated[] = $action;
if ($action instanceof \Generator) {
$this->runGenerator($action);
} elseif ($action instanceof \Closure) {
$this->runClosure($action);
}
}
/**
* Run a queued closure
*
* @param \Closure $closure
* @return void
*/
private function runClosure(\Closure $closure)
{
if ($closure() === self::NEXT) {
$this->initiateNext();
}
}
/**
* Run a queued generator
*
* @param \Generator $generator
* @return void
*/
private function runGenerator(\Generator $generator)
{
foreach ($generator as $result) {
if ($result === self::NEXT) {
$this->initiateNext();
}
}
}
/**
* Initiate the next action
*
* @return void
*/
private function initiateNext()
{
$nextAction = current($this->queued);
next($this->queued);
$this->runAction($nextAction);
}
/**
* Remove the actions
*
* @return void
*/
public function flush()
{
$this->queued = [];
$this->initiated = [];
}
}
Categories: PHP