Reverse Currying
As a followup to last week’s article on curry functions, today’s is about the reverse of currying.
If you try to pass too many arguments to a function, you’ll get a runtime error. Consider this example:
private function foo(): void { var f:Function = addTwo; f(1, 2, 3); } private function addTwo(val1:Number, val2:Number): Number { return val1 + val2; }
Which yields the runtime error:
ArgumentError: Error #1063: Argument count mismatch on addTwo(). Expected 2, got 3.
The reason for the Function variable is that the compiler will do error checking on the number of arguments you pass, but it will not do this error checking on a Function variable. There are many times where you will use a Function variable though, such as callbacks or with some of the Flash API’s functions. Usually, you don’t get to control what gets passed to your function and often you have a function that you don’t want to add useless parameters on to. One way to deal with this is reverse currying. Thankfully, this is easy to do:
private function foo(): void { var f:Function = addTwo; f(1, 2, 3); } private function addTwo(val1:Number, val2:Number, ...rest): Number { return val1 + val2; }
All of the extra parameters will be held in the rest variable, but you can safely ignore them. If only two parameters are passed to addTwo, rest will simply be empty. Var args is really working in our favor here. We can make a companion function to CurryFunction.create to do the reverse currying for us:
package { /** * A utility class to create curried and reverse-curried functions. * * Curried functions are functions that will call another function and add * extra parameters that you define when you create the curried function. * For example: * CurryFunction.create(this, foo, 3, 5)(); * function foo(val:Number, val2:Number): void * { * // val is 3, val2 is 5 * } * * A reverse-curried functions are functions that will call another * function and either remove parameters down to some level or pad * parameters with undefined. * For example: * CurryFunction.reverse(this, addTwo, 2)(1, 2, 3, 4, 5); * function addTwo(val1:Number, val2:Number): Number * { * // 3, 4, and 5 are not passed in * return val1 + val2; * } * * While it is harmless to do so, it is pointless to directly instantiate * a CurryFunction. * * @author Jackson Dunstan (jackson@jacksondunstan.com) */ public class CurryFunction { /** * Create a curried function * @param thisArg The object to call the function on, usually "this" or * "null" for dynamic functions, static functions, or * functions outside a class. * @param func Function to call * @param extraArgs Arguments to curry in. These will be passed after * the arguments normally passed to the function. */ public static function create(thisArg:Object, func:Function, ...extraArgs): Function { return function(...args): * { return func.apply(thisArg, args.concat(extraArgs)); }; } /** * Create a reverse-curried function * @param thisArg The object to call the function on, usually "this" or * "null" for dynamic functions, static functions, or * functions outside a class. * @param func Function to call * @param numArgs Number of arguments to allow through to the function. * This can be used to increase the number of arguments * passed to the function, but all arguments beyond * those actually passed will be undefined. */ public static function reverse(thisArg:Object, func:Function, numArgs:uint): Function { return function(...args): * { args.length = numArgs; return func.apply(thisArg, args); }; } } }
This is rather easy to use:
private function foo(): void { var f:Function = CurryFunction.reverse(this, addTwo, 2); f(1, 2, 3); } private function addTwo(val1:Number, val2:Number): Number { return val1 + val2; }
As with regular currying, there is some overhead to this functionality. This creates an additional dynamic function that acts as a middleman between the caller and the actual function that has been reverse curried. That is expensive, so you should make sure you don’t use currying or reverse currying in any performance-critical code. Using ...rest is faster since it doesn’t involve a middleman function, but will add a little performance hit too. Optimally, performance critical code should always pass the correct number of parameters.
If you added CurryFunction to an AS3 library, go ahead an update it to support reverse currying. If you haven’t added it yet, now you have one more reason. Again, I encourage having more tools in your toolbox because you never know when they’ll come in handy. MXMLC will even drop the CurryFunction class if you never use it, so there’s really little reason not to. I hope this comes in handy for some of you, it sure has for me!
#1 by jonathan on October 9th, 2009 ·
What about optional parameters ?
your (args.length = numArgs;) can be greater than the number of given parameters, is not a problem ?
#2 by jackson on October 9th, 2009 ·
Optional parameters can go two ways. Consider this function:
If you use set numArgs to 1, only one argument will be passed and optional will be defaulted to “n/a”. If you set numArgs to 2 but only pass one argument, optional will not be defaulted but instead set to what you passed: undefined (although this will be converted to null as Strings cannot be undefined). If you set numArgs to 2 and actually pass two arguments, it will be as though you simply called the function with those two arguments. If you set numArgs higher than 2, you will get a runtime error (the same one as in the article) for passing too many parameters.
This last case is the subject of your second question and is answered in the comment for the reverse() function:
The reason for this is that there isn’t a convenient way for reverse() to protect against passing too many parameters since it does not know how many parameters the function takes. There may be some (very slow) way of finding this out, such as describeType(), but I’d rather not add such tremendous overhead to the function.
Thanks for the comment. It’s good to fully explore the details of using such a function. I hope you find it useful.