Tuesday, October 30, 2018

Emulating block scope in PHP

Variables are born, they live their lives, and then they die. But beware! (Cue spoopy theme music.) Zombie variables lurk in the simplest of PHP.


Tonight's code horror opens with pseudo-code for a loop:

array $array = 1..10;
for (int $i=0; $i ≤ count($array); $i++) {
    // do a bunch of stuff
}
echo $i;

What's the output value of $i?

  • The Java dude declares "Simple, it's undefined!"
  • The Javascript gal hesitates "Probably... 10?".
  • The PHP guy, hardened by years of being language-slapped, waffles: "Well, the count of $array is 10, but there could be a break, or a reassignment then break, or $array could be modified in that do a bunch of stuff, so I can't be certain, but I'll agree with the Javascript gal and say 'it should be 10'."

The answer all depends on how the language supports block scope, which is what you get inside an if, while and the like. Java has mandatory block scope. When you declare a variable in a for, that variable exists only in that loop. Javascript has optional block scope, available since ES6 as let. PHP, alas, has no block scope, at all. PHP has global and function scope, and every variable defined in this scope is available in that scope from that point on. This can get messy.

Block-scope helps us reason about the code flow. We know that variables minted in a block scope are guaranteed to not exist outside that block. We can safely use and re-use their names at disparate parts of a function and not worry about refactor bugs. Block-scoping helps make code reasonable, and reasonability is a pillar of well-written code.

Let's look at opted-in block scope inside Javascript, a language familiar to many PHP programmers:

for (let i=0; i<arr.length; i++) {
    // do something here
}
console.log(i); // ReferenceError

Simple. We know that this i cannot interfere with any other i in this current scope. That's a guarantee, and guarantee's are central to analyzing code.

How might we implement this in PHP? Well, the language gives us two scopes, so we need to re-use one of those. The trick is to abuse function scope with an IFFE:

(function () use ($array) {
    for ($i=0; $i<count($array); $i++) {
        // do something here
    }
})();
// $i undefined here

if ($condition) (function () use (&$array) {
    $count = count($array); // $count is local to this condition
    $array[] = 'foo'; // we declared $array pass-by-ref (&$array) in the use
    // etc...
})();

Encase the block you want to define inside an inline function, and run that function. Done. There are two obvious down-sides.

First, there's the verbose use bit to import variables from the outer scope into the inner scope. We can hope that PHP someday gains short-function syntax[1],[2] or automatic import of all outer scope variables to mitigate this concern.

Second is performance. What's the overhead, what's the cost to this safety net? Turns out, in my experiments, it's a little over 2.0%:

<?php

foreach ([1000, 10000, 100000] as $N) {
    $begin = -microtime(true);
    for ($i = 0; $i < $N; $i++) {
        echo uniqid($i);
    }
    fprintf(STDERR, "%d %.6fs\n", $N, $t1 = ($begin + microtime(true)));

    $begin = -microtime(true);
    for ($i = 0; $i < $N; $i++) {
        (function () use ($i) {
            echo uniqid($i);
        })();
    }
    fprintf(STDERR, "%d %.6fs %+.3f%%\n", $N, $t2 = ($begin + microtime(true)), ($t2-$t1)/$t1*100);
}
$ php -d error_reporting=-1 try1.php >/dev/null
1000 0.172234s
1000 0.176464s +2.456%
10000 1.729247s
10000 1.761645s +1.874%
100000 17.303003s
100000 17.670502s +2.124%

I can't say I'll be using this technique. I'd rather just write small functions (25 lines or fewer) and be able to reason about them using just function scope. But not everyone is into hyper-decomposition like I am, so I can see a built-in block-scoping language feature being useful for those cases where less decomposition is required.

What do you think: is this technique ever worth the cost? Should PHP introduce language support to enforce block-scoped variable declarations?

0 comments:

Post a Comment

Share your thoughts!