Starého psa novým kouskům naučíš
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. :)