13
Apr 09

Beware of scoping

So I was bit by ECMA’s scoping rules the other day working with ActionScript. Not because they are hard to understand, but rather because they are not common in today’s programming languages. Most languages have C-like block level scoping, AS3’s lowest scope is function level. This presents some challenges, especially using closures. For the life of me I can’t understand why a language that supports such a beautiful construct as closures, would limit variable scopes to such a high level. Closures are made to capture scope, but in AS3 and Javascript you can only do it at the function level.

What’s more disturbing to me, is that due to the single threaded nature of AS3 and Javascript, the bugs that creep up by not following the correct scoping rules are sometimes hard to detect using tests and/or to a naked eye. Test have to be deterministic, but thread timing really isn’t, so with some unlucky timing or in a different environment, it can blow up in your face.

Here is an example that really emphasizes the issue. We introduce some of this “unlucky” timing explicitly using a scheduler.

for each (var item in [1,2,3,4]) {
   var t:Timer = new Timer(delay, 1);
   t.addEventListener(TimerEvent.TIMER, function(event:TimerEvent):void {
    trace("Item: " + item);
   });
   t.start();
}

So what do you think the above will print? If you think it’ll print

Item: 1
Item: 2
Item: 3
Item: 4

then you’re wrong. It prints

Item: 4
Item: 4
Item: 4
Item: 4

As you can see, this is because the variable item is not scoped to the for each loop, but rather function scope (if it’s defined within a function, otherwise the closes outer scope). This is a major issue, as someone reading this will have a hard time figuring this out, until you realize how closures capture scopes. To fix this issue, you must define the scheduling code inside its own function like below.

function run():void {
  for each (var item in [1,2,3,4]) {
   scheduleMe(item);
  }
}

function scheduleMe(item):void {
  var t:Timer = new Timer(delay, 1);
  t.addEventListener(TimerEvent.TIMER, function(event:TimerEvent):void {
   trace("Item: " + item);
  });
  t.start();
}

The above will work just fine, considering that a new item scoped for each invocation of scheduleMe.

I recommend that one define their variables at their scope level. This might make the code a bit hard to read at first for those so used to more granular scoping, but it will also prevent some oversights as above, considering the programmer knows the scoping rules and doesn’t just try to redefine the variable in the block scope.

Leave a comment