Download

Go here to download ready-to-run and source distributions of Jangaroo. more...

Saturday, January 28, 2012

Simulating ActionScript Parameter Default Values in JavaScript

To continue our series on simulating ActionScript language features in JavaScript, this episode is about parameter default values.
In contrast to JavaScript, ActionScript allows to specify a default value for a function (or method) parameter, like so:

 1 public function insult(s = "fool") {
 2   return "you " + s;
 3 }

The idea is that when the method is called without providing a value for parameter s, the default value "fool" will be used. Details on what additional rules hold for declaring parameter default values are given in the Adobe documentation on function parameters and a bit less reference-like e.g. in a tutorial on Ntt.CC.

Why, it's easy, isn't it?
A straight-forward implementation in JavaScript (similar to the solution suggested by Bernd Paradies for FalconJS) would be to replace undefined paramter values by their default value:

 1 function insult(s) {
     if (s === undefined) {
       s = "fool";
     }
 2   return "you " + s;
 3 }

(We could now start a discussion on whether it shouldn't be
     if (typeof s === "undefined") {
because in JavaScript, undefined may be redefined, but anybody who does so is, excuse me, a fool.)
In JavaScript, when you omit a parameter when calling a function, the parameter value indeed is undefined. But it is a fallacy to assume that every undefined parameter must be replaced by its default value! Consider the following ActionScript class:

 1 public class DefaultParameterTest {
 2
 3   public function DefaultParameterTest() {
 4     trace("1. " +  insult ("nerd"));
 5     trace("2. " +  insult ());
 6     trace("3. " +  insult (undefined));
 7   }
 8
 9   public function insult(s = "fool") {
10     return "you " + s;
11   }
12 }

What do you think will be traced? Try it out, and you'll see that explicitly handing in undefined does not trigger the default value! Thus, the result is
1. you nerd
2. you fool
3. you undefined

(Careful, never call a JavaScript programmer "you undefined"!)
Using the straight-forward JavaScript implementation, the result would be
1. you nerd
2. you fool
3. you fool

As you can see, using parameter default values depends on the number of formal arguments, not on their value. This is the reason why they are also called optional parameters, and why after a parameter with a default value, all following parameters must also specify a default value.

Getting it right
Thus, the correct JavaScript equivalent (as generated by the Jangaroo compiler jooc) is to check the number of formal parameters. Fortunately, this can be realized by checking arguments.length:

 1 function insult(s) {
     if (arguments.length < 1) {
       s = "fool";
     }
 2   return "you " + s;
 3 }

In fact, this solution even provides better runtime performance when using multiple optional parameters, since multiple arguments.length checks can be nested, while the undefined checks in the straight-forward solution would be sequential. Consider the following example:

 1 public function foo(p1, p2, p3 = 3, p4 = 4) {
 2   return p1 + p2 + p3 + p4;
 3 }

The JavaScript fragment generated by Jangaroo looks like so:

 1 function foo(p1, p2, p3, p4) {
     if (arguments.length < 4) {
       if (arguments.length < 3) {
         p3 = 3;
       }
       p4 = 4;
     }
 2   return p1 + p2 + p3 + p4;
 3 }

Jangaroo even optimizes undefined default values, since left out actual parameters are undefined in JavaScript, anyway.

Alternative Solutions
We also thought of using a switch statement, generating code like the following:

 1 function foo(p1, p2, p3, p4) {
     switch (arguments.length) {
       case 0:
       case 1:
       case 2:
         p3 = 3;
         // fall through
       case 3:
         p4 = 4;
     }
 2   return p1 + p2 + p3 + p4;
 3 }

At first sight, this solution seems more efficient, because arguments.length is only evaluated once. The reason why we still chose the nested if code layout is that the switch solution becomes either long or non-robust when using many non-optional parameters. Note the lines case 0: and case 1: in the example above: although the method should never be called with less than two parameters (since the first two are not optional), we want to handle that case, too, since Jangaroo code may be called from JavaScript, where no function signature check is performed. When the function is called with fewer parameters than required, you would still expect the default values to pop in. So the case statements would pile up when there are many non-optional parameters. However, I could imagine a mixed solution like the following:

 1 function foo(p1, p2, p3, p4) {
     switch (Math.max(arguments.length, 2)) {
       case 2:
         p3 = 3;
         // fall through
       case 3:
         p4 = 4;
     }
 2   return p1 + p2 + p3 + p4;
 3 }

The most intelligent solution would be to dynamically decide whether the prior length check pays off against adding several case statements. I guess I'll add that to the Jangaroo backlog!

5 comments:

Bernd Paradies said...

I like your solution better than the one that I was proposing in my blog post at http://blogs.adobe.com/bparadie/2011/11/26/classes-inheritance-interfaces-packages-and-namespaces/. In fact my first implementation was using arguments.length. But back then Falcon was throwing a syntax error for "arguments". My solution was a workaround but I never got around to switching back to arguments.length. Thanks for reminding me!

Bernd Paradies said...

Wait a second... I mixed up a few things. It is true that "arguments.length" was my first attempt and there was a reason why I came up with "undefined" - I just can't remember why I did that. Using "arguments" did trigger syntax errors in Falcon. But that only happened if I used "arguments" in ActionScript. In emitted JavaScript "argument.length" always works. Either way, "argument.length" is the way to go!

Frank said...

Thanks for the acknowledgement, Bernd!
I just committed the "ultimate" version using "if" or "switch", depending on the situation, to jangaroo-tools master, so the next release 0.9.10 will contain that optimization and thus make assigning parameter default values even faster!

James Phillips said...

@Frank: Thanks for the insight on using the length property. I had also made the mistake of assuming I could just simply check to see if the values were undefined/null.

What about uints, ints, and Booleans though? If null is passed for any parameter that’s typed int/uint the value should be 0 and false for Booleans.

It’s possible Jangaroo/FalconJS handle these overrides elsewhere, so that’s why it’s not an issue, but I just thought I’d mention it, as it is something that needs accounted for.

While I was accounting for it, I came up with a different solution for the default param JS code that I thought I’d share. Hopefully it would be easier to update, since it doesn’t output the parameter indices and it eliminates the possibly large switch statement.

ActionScript:
var func:Function=function(intParam:int,stringParam:String=’hi’,...rest:Array) {}

JavaScript:
var func=function(intParam,stringParam)
{
var _total=2, _length=arguments.length, rest=Array.prototype.slice.call(arguments,_total);

if(_length<_total)
{
var _defaultArgs=[undefined,{name:stringParam, value:'hi'}];
while(_length<_total)
{
var _arg=_defaultArgs[_length++];
if(arg) eval(_arg.name+"="+_arg.value+";");
}
}

if(intParam==undefined) intParam=0;
};

Assuming there’s at least one param with a default value in AS, two variables are injected into the JS:

1) _total, which is the total number of params in AS (NOT including any rest param). This value is also used to slice the arguments array to define the ...rest array, since it’s always the last parameter in AS.

2) _length, which is set to arguments.length.

Then, if _length is less than _total, a _defaultArgs array is instantiated (as I loop through the AS params, I push either an object with name and value properties equal to the name of the parameter and its default value or “undefined” if it has no truly defined default value.). Finally, a while loop runs while _length is less than _total. During each iteration it uses/increments the _length variable to locate the corresponding object defined in the _defaultArgs array and evals each name=value;, thus setting any un-passed params to their default values.

The last statement if(intParam) is outside the if statement, so even when func(null); is called, intParam would equal zero just like it does in AS. And, this would work even if intParam has a defaultValue, since it’s executed after the default param code.

Frank said...

> What about uints, ints, and Booleans though?
> If null is passed for any parameter that’s typed
> int/uint the value should be 0 and false for Booleans.

That's right, but as you already assumed, I consider this another problem than parameter default values. Actually, any variable in ActionScript has an initial value depending on its type. Currently, Jangaroo only takes care of initializing typed fields, but it should also initialize typed local variables and, that's your point, typed parameters. In order not to introduce too much runtime overhead, we'd have to do static control flow analysis to only initialize local variables and parameters if necessary.

> While I was accounting for it, I came up with a different
> solution for the default param JS code that I thought I’d
> share.

Thank you for sharing your alternative approach to set parameter default values! Nice approach if you want to do it dynamically, but I'm afraid when generating code (like Jangaroo does), the solution described in my blog post is simpler / more efficient at runtime. Using eval() is not an option for Jangaroo.