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!