Dnešní článek je především reakcí na sérii “From AS3 to C#”, ve které Jackson Dunstan postupně srovnává ActionScript 3 se C#. Leč ActionScript nedisponuje tak bohatou zásobou klíčových slov jako udržovaný C#, ze srovnání nevychází vůbec špatně. Tedy až do 17. dílu, kdy došlo na iterátory potažmo generátory. Jackson tvrdí, že zkrátka není co srovnávat, protože více jak osm let starý ActionScript iterátory/generátory neumí…

Má samozřejmě pravdu. Ale jen částečnou, protože ani jedno z Jacksonových tvrzení (“Iterator function = impossible in AS3”, “Simple loop over iterator function = impossible in AS3”, “Manually call iterator function = impossible in AS3”) není správné.

ActionScript bohužel operátor yield nezná. Na druhou stranu jej stejně tak do nedávna neznal ani JavaScript a přesto v něm existují způsoby, jak lze “generátory” simulovat. Přestaneme-li se na ActionScript dívat jako na staticky typovaný jazyk, který preferuje class-based inheritance, získáme stejný základ - ECMAScript (konkrétně čtvrté generace), ze kterého vychází i JavaScript. No a když známe způsob, jak něco udělat v JavaScriptu, je to už jen krůček k tomu, udělat to i v ActionScriptu:

function range(from:uint, to:uint):Function
{
  return function():uint
  {
    if (from > to) throw new RangeError("done");
    return from++;
  }
}

var g:Function = range(4, 6);
trace(g());// 4
trace(g());// 5
trace(g());// 6
trace(g());// RangeError

Jenom pro srovnání implementace téhož v JavaScriptu vycházejícího z ECMAScriptu šesté generace a používající operátor yield:

function* range(from, to)
{
  to++;
  while (from > to) {
    yield from++;
  }
}

var g = range(4, 6);
console.log(g.next().value);// 4
console.log(g.next().value);// 5
console.log(g.next().value);// 6
console.log(g.next().value);// undefined

Simulovaný generátor není tak přímočarý jako ten skutečný, ale ve výsledku poslouží stejně dobře. Tedy jen do doby, než se ho pokusíme použít v nějakém cyklu. Jediným způsobem, jak v ActionScriptu implementovat vlastní iterátor je rozšířit dynamickou třídu Proxy. Obecně se ji nedoporučuje používat, protože pomalejší věc v AS3 snad neexistuje, ale v našem experimentu účel světí prostředky:

final public dynamic class Generator extends Proxy
{
  private var enumerable:Function;
  private var value:*;
  private var done:Boolean = false;
  private var error:Error;

  public function Generator(enumerable:Function)
  {
    this.enumerable = enumerable;
  }

  override flash_proxy function nextValue(index:int):*
  {
    return new GeneratorResult(value, done);
  }

  override flash_proxy function nextNameIndex(index:int):int
  {
    yield();
    return int(!done);
  }

  public function next(sent:* = undefined):GeneratorResult
  {
    yield(sent);
    return new GeneratorResult(value, done);
  }

  private function yield(sent:* = undefined):void
  {
    try {
      value = enumerable(sent);
      if (value == undefined) {
        throw new ReferenceError("yield is not defined");
      }
    } catch (error:RangeError) {
      done = true;
      value = null;
    } catch (error:*) {
      done = true;
      value = null;
      this.error = error;
    }

    if (this.error) {
      throw this.error;
    }
  }
}

final public class GeneratorResult
{
  private var _value:*;
  private var _done:Boolean;

  public function GeneratorResult(value:*, done:Boolean)
  {
    _value = value;
    _done = done;
  }

  public function get value():*
  {
    return _value;
  }

  public function get done():Boolean
  {
    return _done;
  }

  public function toString():String
  {
    return "[value=" + value + "; done=" + done + "]";
  }
}

Nyní upravíme původní implementaci funkce range tak, aby namísto nějaké funkce vracela instanci třídy Generator (použití je pak téměř stejné jako v nadcházející verzi JavaScriptu):

function range(from:uint, to:uint):Generator
{
  return new Generator(function():uint
  {
    if (from > to) throw new RangeError("Range error.");
    return from++;
  });
}

Můžeme iterovat pomocí cyklu:

for each (var i:uint in range(4, 6)) trace(i);

Ručně:

var g:Generator = range(4, 6);
trace(g.next().value);
trace(g.next().value);

Nebo tak trochu oběma způsoby:

var g:Generator = range(4, 6);
var v:GeneratorResult;
while (!(v = g.next()).done) trace(v.value);

Dokonce můžeme poslat zprávu:

function range(from:uint, to:uint):Generator
{
  var origFrom:uint = from;

  return new Generator(function(message:String):uint
  {
    if (message == "reset") from = origFrom;
    if (from > to) throw new RangeError("Range error.");
    return from++;
  });
}

var g:Generator = range(4, 6);
trace(g.next().value);// 4
trace(g.next().value);// 5
trace(g.next("reset").value);// 4
trace(g.next().value);// 5

A pak že není co srovnávat. Nicméně osobně bych to asi nikdy nepoužil. :)