Cancelable Function
I recently had the need to cancel a callback function that I had passed to an API. The API had taken my callback function directly, so there was no way to remove the event listener. So I thought back to an old article I wrote and came up with a solution. Read on for a utility function that will allow you to cancel function callback in AS3 as well as JavaScript and AS2.
I’m sure you’ve all seen the sort of API I’m talking about. It looked something like this:
function loadDisplayObject(url:String, callback:Function): void { var loader:Loader = new Loader(); loader.contentLoaderInfo.addEventListener( Event.COMPLETE, function(ev:Event): void { callback(loader.content); } ); // ... more handlers for errors, etc. loader.load(new URLRequest(url)); }
That’s a handy way of easily loading a DisplayObject (SWF, JPEG, PNG, etc.) because you simply need to write one line:
loadDisplayObject( "foo.swf", function(result:DisplayObject): void { // do something with the result } );
But how do you cancel the callback? Were you dealing with the Loader directly, you could simply call removeEventListener and stop yourself from being called back. Assuming you can’t change the API, you need to get creative and come up with a way to cancel the Function callback yourself. Remembering that old article I wrote, I came up with an idea to add a cancel method to the Function in AS3:
/** * Utility class for creating cancelable functions * @author Jackson Dunstan */ public class CancelableFunction { /** * Create a cancelable function. When called, its arguments will be * passed along to the given function and the given function's return * value will be returned. A cancelable function can also be canceled * like so: * var func:Function = CancelableFunction.create(myFunc); * func["cancel"](); * If canceled, calling the function has no effect and returns * undefined. * @param func Function to call when calling the cancelable function * @return The cancelable function * @author Jackson Dunstan */ public static function create(func:Function): Function { var ret:Function = function(...args): * { if (func != null) { return func.apply(func, args); } }; ret["cancel"] = function(): void { func = null; }; return ret; } }
Usage, as described in the AsDoc block, is simple:
var func:Function = CancelableFunction.create(myFunc); func["cancel"]();
The first step is to create a utility function that takes a function F and returns a cancelable version U. When U is called it calls F and passes all arguments passed to U and then returns whatever value F returned. We then dynamically add on a property to U using the index [] operator. We name the property cancel and assign it a function that simply sets F to null as a way of flagging that it should not be called back by U. We then tweak U to check if its callback is null before calling it. The result is a cancelable function!
Let’s see if this all works with a little test app in AS3:
package { import flash.display.*; import flash.text.*; public class CancelableFunctionTest extends Sprite { private var logger:TextField; public function CancelableFunctionTest() { logger = new TextField(); logger.autoSize = TextFieldAutoSize.LEFT; addChild(logger); function dynamic_print0(): void { log("dynamic_print0"); } function dynamic_print1(one:int): void { log("dynamic_print1: " + one); } function dynamic_print2(one:int, two:int): void { log("dynamic_print2: " + one + ", " + two); } function dynamic_print2ret(one:int, two:int): int { log("dynamic_print2ret: " + one + ", " + two); return 3; } log("dynamic:"); CancelableFunction.create(dynamic_print0)(); CancelableFunction.create(dynamic_print1)(100); CancelableFunction.create(dynamic_print2)(100, 200); log("\tand the ret: " + CancelableFunction.create(dynamic_print2ret)(100, 200)); log("method:"); CancelableFunction.create(method_print0)(); CancelableFunction.create(method_print1)(100); CancelableFunction.create(method_print2)(100, 200); log("\tand the ret: " + CancelableFunction.create(method_print2ret)(100, 200)); log("dynamic, canceled:"); var func:Function; func = CancelableFunction.create(dynamic_print0); func["cancel"](); func(); func = CancelableFunction.create(dynamic_print1); func["cancel"](); func(100); func = CancelableFunction.create(dynamic_print2); func["cancel"](); func(100, 200); func = CancelableFunction.create(dynamic_print2ret); func["cancel"](); log("\tand the ret: " + func(100, 200)); log("method, canceled:"); func = CancelableFunction.create(method_print0); func["cancel"](); func(); func = CancelableFunction.create(method_print1); func["cancel"](); func(100); func = CancelableFunction.create(method_print2); func["cancel"](); func(100, 200); func = CancelableFunction.create(method_print2ret); func["cancel"](); log("\tand the ret: " + func(100, 200)); } private function log(msg:*): void { logger.appendText(msg + "\n"); } private function method_print0(): void { log("method_print0"); } private function method_print1(one:int): void { log("method_print1: " + one); } private function method_print2(one:int, two:int): void { log("method_print2: " + one + ", " + two); } private function method_print2ret(one:int, two:int): int { log("method_print2ret: " + one + ", " + two); return 3; } } }
When run, this does give the expected results:
dynamic: dynamic_print0 dynamic_print1: 100 dynamic_print2: 100, 200 dynamic_print2ret: 100, 200 and the ret: 3 method: method_print0 method_print1: 100 method_print2: 100, 200 method_print2ret: 100, 200 and the ret: 3 dynamic, canceled: and the ret: undefined method, canceled: and the ret: undefined
I found this useful, so I ported it to JavaScript:
/** * Create a cancelable function. When called, its arguments will be * passed along to the given function and the given function's return * value will be returned. A cancelable function can also be canceled * like so: * var func = CancelableFunction.create(myFunc); * func.cancel(); * If canceled, calling the function has no effect and returns * undefined. * @param func Function to call when calling the cancelable function * @return The cancelable function * @author Jackson Dunstan */ var CancelableFunction = { create: function(obj, func) { var ret = function() { if (func != null) { return func.apply(obj, arguments); } }; ret.cancel = function() { func = null; }; return ret; } };
As you can see, the JavaScript version is even more straightforward in that you don’t have to index (using []) in to the Function. Instead, you can simply use the dot (.) operator to cancel the function. Here is the ported version of the test, which yields the same results as the AS3 version except for the lack of method tests:
function log(msg) { document.getElementById("logger").innerHTML += msg + "<br/>"; } function dynamic_print0() { log("dynamic_print0"); } function dynamic_print1(one) { log("dynamic_print1: " + one); } function dynamic_print2(one, two) { log("dynamic_print2: " + one + ", " + two); } function dynamic_print2ret(one, two) { log("dynamic_print2ret: " + one + ", " + two); return 3; } log("dynamic:"); CancelableFunction.create(dynamic_print0)(); CancelableFunction.create(dynamic_print1)(100); CancelableFunction.create(dynamic_print2)(100, 200); log("\tand the ret: " + CancelableFunction.create(dynamic_print2ret)(100, 200)); log("dynamic, canceled:"); var func; func = CancelableFunction.create(dynamic_print0); func["cancel"](); func(); func = CancelableFunction.create(dynamic_print1); func["cancel"](); func(100); func = CancelableFunction.create(dynamic_print2); func["cancel"](); func(100, 200); func = CancelableFunction.create(dynamic_print2ret); func["cancel"](); log("\tand the ret: " + func(100, 200));
I then ported it to AS2:
/** * Utility class for creating cancelable functions * @author Jackson Dunstan */ class CancelableFunction { /** * Create a cancelable function. When called, its arguments will be * passed along to the given function and the given function's return * value will be returned. A cancelable function can also be canceled * like so: * var func:Function = CancelableFunction.create(myFunc); * func["cancel"](); * If canceled, calling the function has no effect and returns * undefined. * @param func Function to call when calling the cancelable function * @return The cancelable function * @author Jackson Dunstan */ public static function create(func:Function): Function { var ret:Function = function(): Object { if (func != null) { return func.apply(func, arguments); } }; ret["cancel"] = function(): Void { func = null; }; return ret; } }
Here is the test code for the AS2 version, which yields identical results to the AS3 version. It is designed to be built with MTASC.
class CancelableFunctionTest { private static function log(msg:Object): Void { _root.logger.text += msg + "\n"; } function CancelableFunctionTest() { var dynamic_print0:Function = function(): Void { CancelableFunctionTest.log("dynamic_print0"); } var dynamic_print1:Function = function(one:Number): Void { CancelableFunctionTest.log("dynamic_print1: " + one); } var dynamic_print2:Function = function(one:Number, two:Number): Void { CancelableFunctionTest.log("dynamic_print2: " + one + ", " + two); } var dynamic_print2ret:Function = function(one:Number, two:Number): Number { CancelableFunctionTest.log("dynamic_print2ret: " + one + ", " + two); return 3; } log("dynamic:"); CancelableFunction.create(dynamic_print0)(); CancelableFunction.create(dynamic_print1)(100); CancelableFunction.create(dynamic_print2)(100, 200); log("\tand the ret: " + CancelableFunction.create(dynamic_print2ret)(100, 200)); log("method:"); CancelableFunction.create(method_print0)(); CancelableFunction.create(method_print1)(100); CancelableFunction.create(method_print2)(100, 200); log("\tand the ret: " + CancelableFunction.create(method_print2ret)(100, 200)); log("dynamic, canceled:"); var func:Function; func = CancelableFunction.create(dynamic_print0); func["cancel"](); func(); func = CancelableFunction.create(dynamic_print1); func["cancel"](); func(100); func = CancelableFunction.create(dynamic_print2); func["cancel"](); func(100, 200); func = CancelableFunction.create(dynamic_print2ret); func["cancel"](); log("\tand the ret: " + func(100, 200)); log("method, canceled:"); func = CancelableFunction.create(method_print0); func["cancel"](); func(); func = CancelableFunction.create(method_print1); func["cancel"](); func(100); func = CancelableFunction.create(method_print2); func["cancel"](); func(100, 200); func = CancelableFunction.create(method_print2ret); func["cancel"](); log("\tand the ret: " + func(100, 200)); } static function main(mc:MovieClip) { _root.createTextField("logger", 0, 0, 0, 800, 600); var test:CancelableFunctionTest = new CancelableFunctionTest(); } private function method_print0(): Void { log("method_print0"); } private function method_print1(one:Number): Void { log("method_print1: " + one); } private function method_print2(one:Number, two:Number): Void { log("method_print2: " + one + ", " + two); } private function method_print2ret(one:Number, two:Number): Number { log("method_print2ret: " + one + ", " + two); return 3; } }
This utility function helped me out of a bind when an API wouldn’t let me cancel a callback. Next time you find yourself in a similar situation, don’t add yet-another flag field to your class to keep track of some callback, try canceling the function altogether!
#1 by y_nk on May 11th, 2010 ·
Hello,
This looks very usefull thanks. Why not overriding the Function prototype to add this feature natively ? Function class is dynamic, it shouldn’t be a problem. There’s a french article about prototypes in as3, you may find the examples usefull : http://bit.ly/aVY0Hw
#2 by jackson on May 11th, 2010 ·
That’s definitely another option. Personally, I prefer to change only the objects I need to change rather than globally changing all Functions. If you prefer differently, it should be easy to apply this technique using the Function prototype. If you do, would you mind posting your implementation here?
#3 by Robert Penner on May 28th, 2010 ·
This is a cool idea, and it’s great to see it in multiple languages.
A small point–
loader.addEventListener(Event.COMPLETE,
–should be:
loader.contentLoaderInfo.addEventListener(Event.COMPLETE,
I blame Macromedia for this one, though. It’s so unintuitive.
#4 by jackson on May 28th, 2010 ·
Good catch. Clearly I never actually ran that code. :) And I totally agree about contentLoaderInfo and the whole process of doing a load. I knew almost as soon as I started loading (a lot) with AS3 that I’d need to abstract all that into something more like LoadHelper.load(url, callback).
#5 by Robert Penner on May 28th, 2010 ·
The great thing about AS3 events is that you can add listeners for the wrong events and nothing lets you know–compile or run-time. You just sit around wondering why nothing’s happening.
#6 by jackson on May 28th, 2010 ·
Good thing we have as3signals and TurboSignals! ;-)
#7 by OffibrerafRak on January 7th, 2011 ·
хороший у Ð²Ð°Ñ Ð±Ð»Ð¾Ð³, быду заходить