Default Argument Expressions Are Executed Only As-Needed In ColdFusion
For all the years that I've been writing ColdFusion code, I have always just assumed that default-value expressions for optional arguments were executed whether or not they were needed. I assumed that the only part of the default value that was conditional was the assignment of the default value to the missing argument reference. This morning, however, I finally sat down to test this assumption. And, I am both saddened and excited to find out that I was completely wrong. What's more, my assumption was incorrect for both Script and Tag contexts!
To test this, I created a function that took a set of optional arguments. For each argument that was omitted, the function would provide a default value that would track its own usage. The result of the function returned the number of default argument expressions that had been executed:
<cfscript>
// I return a default value for use in an optional argument.
numeric function getDefaultValue() {
return( ++defaultValue );
}
// I allow for a variable number of arguments to be provided, with
// the missing arguments to be given default value via a function
// expression.
numeric function testDefaultArgumentExpressions(
numeric a = getDefaultValue(),
numeric b = getDefaultValue(),
numeric c = getDefaultValue(),
numeric d = getDefaultValue(),
numeric e = getDefaultValue()
) {
return( defaultValue );
}
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// Since the defaultValue is pre-incremented above, we will be
// able to see how many times the getDefaultValue() method is
// invoked.
defaultValue = 0;
// Pass in THREE out of FIVE optional arguments.
result = testDefaultArgumentExpressions(
a = 101,
b = 102,
c = 103
);
// At this point, "result" will report the number of times the
// getDefaultValue() expression was executed.
writeOutput( "Script Result: " & result );
</cfscript>
As you can see, each omitted argument increments the value of "defaultValue." The test method then returns the final defaultValue, indicating how many times the default value expression was invoked. When we run the above code, we get the following output:
Script Result: 2
As you can see, the getDefaultValue() method was invoked only twice for the 2-out-of-5 arguments (the ones that were omitted).
The same holds true in a ColdFusion tag context:
<cffunction
name="getDefaultValue"
returntype="numeric"
hint="I return a default value for use in an optional argument.">
<cfreturn ++defaultValue />
</cffunction>
<cffunction
name="testDefaultArgumentExpressions"
returntype="numeric"
hint="I allow for a variable number of arguments to be provided, with the missing arguments to be given default value via a function expression.">
<cfargument name="a" default="#getDefaultValue()#" />
<cfargument name="b" default="#getDefaultValue()#" />
<cfargument name="c" default="#getDefaultValue()#" />
<cfargument name="d" default="#getDefaultValue()#" />
<cfargument name="e" default="#getDefaultValue()#" />
<cfreturn defaultValue />
</cffunction>
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!---
Since the defaultValue is pre-incremented above, we will be able
to see how many times the getDefaultValue() method is invoked.
--->
<cfset defaultValue = 0 />
<!--- Pass in THREE out of FIVE optional arguments. --->
<cfset result = testDefaultArgumentExpressions(
a = 101,
b = 102,
c = 103
) />
<!---
At this point, "result" will report the number of times the
getDefaultValue() expression was executed.
--->
<cfset writeOutput( "Tag Result: " & result ) />
In the tag context, it's hard to believe that the default-value expression is optionally executed because the value is defined using ## symbols. In every other tag, this would almost definitely indicate expression evaluation. Yet, when I run the above code, I get the following output:
Tag Result: 2
This is somewhat shocking. It's almost like ColdFusion is implicitly wrapping the Default tag attribute in a delayed-evaluation context. This has to be some black-magic in the compilation process because I don't think that this kind of behavior is something that a developer can reproduce in, say, a Custom Tag setting. At least, not without explicitly using the DE() method (for "Delayed Evaluation").
Regardless of how ColdFusion is implementing the default-value binding, this finding is fairly exciting for me. On the one hand, I happen to love finding out that my assumptions are wrong (hello, "growth moments!"); but, on the other hand, this definitely means that my Default values can be more complex if they need to be without the fear of unwanted invocation.
Want to use code from this post? Check out the license.
Reader Comments
This is kinda like cfparam behaving as the cfelse condition of a cfif IsDefined. Or like short-circuited booleans.
Sometimes it's really impressive the extent to which ColdFusion avoids executing unnecessary code. Like cfinclude, for example:
When you think about it, CFML doesn't behave like other languages. If you import something into a java file, it's imported unconditionally. Same with header files in C or COPY statements in COBOL. But if you cfinclude something into a ColdFusion file, it's only cfncluded if execution hits that statement.
Similarly, you don't crash on a variable not being defined until you hit that statement, allowing it to have been defined in all sorts of ways invisible to the compiler.
Kinda like a JavaScript level of freedom, when you think about it.
What you've discovered is, that very same freedom results in less code execution, and therefore is also more efficient! Amazing!
@WebManWalking,
Yeah, it's definitely pretty cool! When I find stuff like this, I always wonder, did I find something unexpected? Or was my original assumption completely unfounded to begin with? It's stuff like this that makes you realized that compilers are doing a lot more than you might think they are doing.
I wonder how simple the ColdFusion code might actually be. It's a bit like being in awe of how it can execute any SQL command, only to realize that all it's doing is passing the command to SQL, having SQL execute the command and then returning the result.
I wonder if ColdFusion is simply a pass-thru to Java the way it's a pass-thru to the database.
@Phillip,
That's an interesting question. I don't know enough about Java and the way Java handles default argument values... or even IF Java allows for default argument values.
I would have been more surprised if it worked any other way!
You certainly don't want that default value to be computed when the argument is provided and it's not at all magic that it isn't. It's just a conditional. Inside the function, it might as well be:
if argument not provided
then
argument = default value
endif
@Sean,
From a philosophical standpoint, I agree with you - that is how one might expect it to work. But, with such a long history of Tags and attribute evaluation, I think it's easy to not jump to the appropriate conclusion.
That said, this morning I took a quick look at the CFParam tag, which also provides a "Default" attribute; before yesterday, I would have told you that CFArgument and CFParam work in basically the same way. In fact, in the past, I've used CFParam to *define arguments* inside a function for a variable-signature:
www.bennadel.com/blog/1756-Using-CFParam-To-Define-A-Variable-Number-Of-Arguments-In-ColdFusion-And-What-ColdFusion-9-Teaches-Us-.htm
... but that, said, I tried to put the CFParam through the same experiment (as this blog post with the CFArgument tags):
www.bennadel.com/blog/2515-Default-CFParam-Expressions-Are-Always-Executed-In-ColdFusion.htm
When it comes to the CFParam tag, the Default expression is always evaluated, even if it is not required.
@Phillip,
What you're saying about ColdFusion's support for SQL would be true if it weren't for the fact that CFML can be nested inside of a cfquery. In my work environment, it's routine to say stuff like
I'm avoiding less-than and greater-than due to tag restrictions here, but you get the idea. That logic isn't just passed through as-is to the datasource.
And the king tag nestable inside cfquery is cfqueryparam. It does SQL injection rejection, script tag rejection, db-vendor-specific data reformatting, etc. What a little powerhouse it is!
If you want to experience SQL being passed through as-is to a database, try PHP for a change. It has different, incompatible database drivers for every DB vendor. It is, simply put, awful. Nothing makes you appreciate ColdFusion's DB vendor abstraction and cfqueryparam's syntax stratification quite as much as having to code in PHP.
But in response to what you were wondering, Java uses call-by-position. Methods can be overloaded using "signatures" based on data types that allow arguments to be optional, but you'd have to be an idiot to implement optional argument using signatures. The combinatorial explosion alone would kill you if somehow managed to distinguish multiple arguments of the same type.
No. ColdFusion's implementation of default values is not a simple pass-through to some magical Java call mechanism. ColdFusion does it for you, and quite well, Ben discovered.
@WebManWalking,
"ColdFusion does it for you,"... that's why CF is so awesome :)