Using CFParam To Define A Variable Number Of Arguments In ColdFusion (And What ColdFusion 9 Teaches Us)
Yesterday, I was watching an episode of "Flex and the City" (a female bodybuilding spoof on "Sex and the City") when I started to think about ColdFusion function definitions with a variable number of arguments. Most of the time, when we need to have a variable number of arguments in a method signature, we accomplish this by giving the subsequent CFArgument tags a required="false" attribute. This, however, only works well when the optional arguments appear after the required one and in an exacting order.
When we have a function signature in which an optional argument may precede a required argument, we either have to force the user to invoke the method using named arguments (something I have done in the past), or we have to start to do some fenagling. That fenagling that I've done in the past has not been pretty; but then last night, it suddenly occurred to me that I could use ColdFusion's CFParam tag to define named arguments based on a variable number of ordered arguments.
ColdFusion's CFArgument and CFParam tags are actually quite similar; both allow us to define variable names, data types, default values, and whether or not the given variable is required. In fact, just about the only difference between the two tags is that the CFArgument tag lets us use a Hint attribute (and that the CFArgument tags will show up in the CFFunction meta data). Thanks to this similarity, I realized that I could actually use the CFParam tag in lieu of the CFArgument tag in cases where I am not sure at compile time as to which ordered arguments will correspond to which named arguments.
To play around with this idea, I created the function getRandomNumber(). The getRandomNumber() function returns a random value in a given range and can be called with the three following signatures:
getRandomNumber()
In this invocation, the range is implied to be zero to one.
getRandomNumber( to )
In this invocation, the From limit is implied to be zero.
getRandomNumber( from, to )
In this invocation, the range is defined purely by the arguments.
As you can see, not only do we have a variable number of arguments but, the optional argument - from - comes before another optional argument - to - but only in one of the cases. To deal with this as elegantly as possible, I tried using CFParam tags instead of CFArgument tags:
<cffunction
name="getRandomNumber"
access="public"
returntype="numeric"
output="true"
hint="I return a random number between the given (or implied) range.">
<!--- Define the local scope. --->
<cfset var local = {} />
<!---
Check to see how many arguments we have. This method can
be invoked with a variable number of arguments:
- getNumber()
- getNumber( to )
- getNumber( from, to )
--->
<cfif (arrayLen( arguments ) eq 2)>
<!---
Two arguments. This will be the FROM and TO limits of
the random number range.
--->
<cfparam
name="arguments.from"
type="numeric"
default="#arguments[ 1 ]#"
/>
<cfparam
name="arguments.to"
type="numeric"
default="#arguments[ 2 ]#"
/>
<cfelseif (arrayLen( arguments ) eq 1)>
<!---
One argument. This will be the TO limit of the random
range. The from will default to zero.
--->
<cfset arguments.from = 0 />
<cfparam
name="arguments.to"
type="numeric"
default="#arguments[ 1 ]#"
/>
<cfelse>
<!---
No arguments. This will select between zero and one,
returning what is basically a boolean random.
--->
<cfset arguments.from = 0 />
<cfset arguments.to = 1 />
</cfif>
<!--- Return the randomly selected value. --->
<cfreturn randRange( arguments.from, arguments.to ) />
</cffunction>
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!--- Output some random numbers. --->
<cfoutput>
#getRandomNumber()#<br />
#getRandomNumber( 10 )#<br />
#getRandomNumber( 0, 100 )#<br />
</cfoutput>
As you can see in this code sample, I am not defining any CFArgument tags. Rather, I am checking to see how many arguments have been passed-in, and then I'm using the CFParam tag to translate the ordered arguments into named arguments. You might wonder why I am using the CFParam tag rather than just using the CFSet tag; this is for data type checking via the Type attribute. CFArgument enforces type checking and I didn't want to lose that feature.
When we run the above code, we get the following output:
1
6
79
The only reason that this works is because the Arguments struct in ColdFusion is somewhat magical in that it can lookup values using either an index or a key. But, at the same time, we can run into problems if a user passes in a named-argument when only an ordered argument was expected. In fact, the safest route in the above function would have been to CFParam the parameter values into the LOCAL scope rather than back into the Arguments scope.
This is not the most attractive code; but, when your optional arguments don't appear in a consistent order, I think this is as clean as it gets. But, this does beg the very interesting question - is it OK to force the programmer to invoke a method using Named Arguments? If you look at ColdFusion 9's CFScript-based implementation of the CFMail, CFFTP, CFHTTP, and CFQuery tags, you will notice that their init() methods all look exactly the same:
// INIT() method copied from ColdFusion 9's CFScript-based
// tag implementation.
function init()
{
if(!structisempty(arguments))
{
structappend(variables,arguments,"yes");
}
return this;
}
As you can see here, all of ColdFusion 9's CFScript-based tags that are implemented as ColdFusion components rely completely on the fact that the user invokes the constructor using named arguments. In fact, any attempt to use ordered arguments here would either throw an exception or lead to a very unexpected outcome. So does this set the precedence that requiring named arguments is acceptable? Certainly, is makes defining CFArgument tags much easier. I wonder?
Want to use code from this post? Check out the license.
Reader Comments
I think named arguments are the way to go. When I programmed C++, I hated that you had to remember the order of arguments and pass in nulls for ones you didn't need and such. Being able to specify getRandomNumber(from=1,to=100) is much better in my mind. It also prevents errors later if you add in new arguments to a function: you can't disrupt arguments passed by name, but you could if they were passed by order.
@Jon,
If I had to pass in null values, I would be miserable :) But, the awesome dynamic nature of ColdFusion functions is that there is not *true* method signature; as such, you can invoke a method with a variable number of arguments without null values. As such, I don't know if we should draw conclusions based on other languages.
As far as preventing errors when adding arguments later on, that is a good point. That's something that having a true method signature actually helps with as that becomes a completely different method.
If we look at Javascript as another powerful, dynamic language, esepcially in the jQuery world, what we see is that people have started using option hashes - passing in structs as a single argument which contains many name-value arguments. This is powerful for the reasons you mention and basically works the same as named arguments in ColdFusion... so, perhaps we should take a pointer or two from them.
I'm all for named arguments. I use them pretty much everywhere. It might add to the verbosity of calling the functions but it's so handy when you want to extend the signature of the method later (which invariably you will do).
Even in complex situations I know that adding another argument to a function will not compromise the existing code. No need to trawl the code worrying about the calling functions.
Used correctly it's a very powerful feature of the language.
@Aidan,
Don't worry - I am certainly not one who minds a little bit of verbosity here and there (I find it to be very explanatory in the code).
Sorry to repeat it Ben, but named arguments really is the bee's knees.
I took a PHP class recently and all the example code used ordered arguments, which got totally insane as the examples increased in complexity. I had sworn off PHP entirely until I realized you could pass in a single argument containing an array of arguments that can be accessed by name.
Of course, that doesn't come close to the amount of functionality you get with cfargument.
@Russ,
Yeah, that's the beauty of the arguments collection in ColdFusion - works as both an ordered AND a name-value set. Way badass. But I guess it sounds like people are really liking the named arguments.
But, I wonder if flexibility in future changes to the method signature should really be weighed too heavily. Or rather, should that be our ultimate goal? Remember, the majority of CF-functions that come with the application server are done via ordered arguments, NOT named arguments (and we've all pretty much loved those).
@Ben: Any guesses as to how often I'd like to be able to pass named arguments to the built in CF functions? I hate needing to say ImageNew("", 200, 200, "rgb", "red"), when I could just say ImageNew(width=200,height=200)? It might not be shorter, bur its more accurate, and doesn't make you scratch your head remembering what the "" is for at the beginning (especially since you don't think about copying an image using a "new" command). All of those arguments are supposed to be "optional", but since you go in order, you are required to pass them all in; the arguments aren't really optional in practice.
I mean, we can selectively pass in which attributes of a tag we want to use, and we can do that with our UDFs, but we can't do it with the predefined ones? The arguments have names in the LiveDocs, why can't we use them by name?
@Jon,
You make a really good point. There are definitely a lot of times in the existing language where using named arguments would not only be easier, but would also lend much more insight into what the arguments were for.
I think what we're seeing (if I may sum it up):
* Using ordered-arguments is very nice and concise when we the number of arguments is very small and static.
* Forcing the user to use named-arguments is seen as completely acceptable.
I'm cool with; no need to go 100% one way or the other.
I love the creativity at work here.
The only problem I have is what is driving this great solution, the issue of calling a function with out naming the parameters. I'm a proponent that code be as readable as possible. If you have an array of person objects, call it PersonArray. Or, like in this case, be explicit about which values are for which arguments. It really helps when people not as familiar with the application work on it. There's no need to open up the function and dig through it's code to figure out valid arguments.
The other thing here is if you have a function that has a lot of optional parameters, it's time to think about breaking it up. The example you used is great. But a function really should be "a function". The more optional arguments it is, the more likely it needs to be broken up.
But like you said, it's a matter of preference. It's not right or wrong.
@Allen,
This is true - I think when we start to have a lot of optional arguments, we really enter new territory; we're not using the function as a "function", we're using it as something more comprehensive in which the arguments are really "configuration options".
Perhaps this in arbitrary separation, but there is something that feels natural about it.
@Ben,
Just because you can use CFParam in functions doesn't necessarily make it right ... in fact, it just feels wrong.
While I understand what you're trying to accomplish, I think as a proponent of "best practices" you would be better served going down the CFArgument path as you pointed out earlier on. I personally would be rewriting code that had CFParams littered throughout any functions thinking that the person who wrote it didn't really understand how functions are supposed to work.
So there's my nickel contribution.
@Steve,
The CFParam is used purely because it creates a concise combination of value setting and data type validation. Yes, you could do that without CFParam, such as with:
<cfset local.to = arguments[ 1 ] />
<cfif !isNumeric( local.to )>
. . . . <cfthrow type="NonNumericValue" />
</cfif>
... But certainly, this doesn't *feel* better than using the CFParam tag (I hope we can agree on that).
The only issue with using the CFArgument tag, as we've gone back and forth over in the above comments, is that you require the user to use named arguments rather than ordered arguments. If you were to use ordered arguments, you'd have to do something like this (brief markup):
<cfargument name="from" />
<cfargument name="to" />
<cfif !arrayLen( arguments )>
. . . . <cfset arguments.from = 0 />
. . . . <cfset arguments.to= 1 />
<cfelseif (arrayLen( arguments ) eq 1)>
. . . . <cfset arguments.to= arguments.from />
. . . . <cfset arguments.from = 0 />
</cfif>
Here, because our *optional* arguments appear in a pre-pending order as you need them, if there is only one argument, you have to assign the second argument to the first one and then override the first one.
I think we can also agree that *this* does not feel better than CFParam.
As such, I just want to be on the same page that when you say:
code that had CFParams littered throughout any functions thinking that the person who wrote it didn't really understand how functions are supposed to work.
... are you referring to forcing the programmer to use named-arguments when they invoke the function?
@Ben,
Can't you still enforce value setting and data validation with CFArgument though (regardless of the fact that they're ordered)? For example:
<cfargument name="from" type="numeric" required="false" default="0" />
<cfargument name="to" type="numeric" required="false" default="100" />
And yes, I'm really forcing the programmer to use named-arguments as you said. Or, they could always pass in an attributeCollection if they want as well, which is how I like to do it if there are more than a few attributes being called anyway.
Or do you think that our functions should be 'smart-enough' to figure out which attribute should receive the value that's being passed in all by itself somehow?
I forgot to add an example. So, if someone were to call getRandomNumber(100,5) ... do you think the function should 'know' that 100 should be the 'to' attribute and the 5 should be the 'from'?
@Steve,
Yeah, you can use the data validation on the CFArgument tags, regardless of order, if they invoked by name (which is what the ArgumentCollection is also doing).
I wasn't saying either-or; this was just an exploration of ideas. There are definitely a lot of times where there are so few arguments that I like the idea of using ordered; but, it seems like the larger and more dynamic the set of arguments, requiring people to use named-arguments is totally cool.
Again, not an either-or, just a good conversation.
@Steve,
No no, not *that* smart. But, if you call it as (N) vs. (N,M), the function should understand that N is the "to" in the first case and M is the "to" in the second case.
@Ben,
Then just rearrange the arguments in this case and you'd be all set:
<cfargument name="to" type="numeric" required="false" default="100" />
<cfargument name="from" type="numeric" required="false" default="0" />
Now calling getRandomNumber(800) would work and getRandomNumber(800,100) would work too.
@Steve,
Not quite because in the latter example, the 800 would be mapped to the ordered-argument "to".... oh, I see what you're doing - you invoking it in the form of:
getRandomNumber( to [, from ] )
Hmmm. Interesting idea. My only concern would be that having the range backwards would not be intuitive. But yes, that would solve all the problems.
oh, wait, after re-reading your comment, i see what you're saying. you really do want the function to be smart enough in this case then.
so calling getRandomNumber(100,800) or getRandomNumber(800,100) you would expect it to know on its own somehow.
and this is where i don't agree with you ... i don't think this should work both ways. well, in this simple case you could easily add something like if 'to' lt 'from' then reassign the vars and such ... but i'm not certain that would be a good idea.
@Steve,
Wait, I am not sure we are on the same page - I don't want the function to work regardless of the order of the arguments. I want it to work in one of three ways:
fn()
fn( to )
fn( from, to )
I have no interest in it understanding that ( to, from ) would work as well.
@Ben,
Right, I understand you, but I don't agree with you.
I don't think the last function should work ... unless you explicitly name the vars.
So assuming the function is set up as getRandomNumber( to [, from ] ) and you call this
getRandomNumber(from=10,to=800) ... that's fine by me. But I don't think getRandomNumber(10,800) should work.
"No no, not *that* smart. But, if you call it as (N) vs. (N,M), the function should understand that N is the "to" in the first case and M is the "to" in the second case." -- @Ben
To me, this illustrates why I don't like going this route. Yes, the function can understand that N it the "to" and the "M" is the second. But how does another developer understand this?
I realize it might seem petty but there's a good chance it's going to take another developer extra time to dig into things and figure out which is which, etc. Ya, I know, small. But think of some scenario, like you're on a team trying to crank through the last 50 bugs in 3 days before some big product release date. That extra 3 minutes here, 8 minutes there starts to add up.
Does the programmer that saves time upfront by not having to type out those named arguments really save time or are they just costing someone else time?
Again, not trying to single you out Ben as doing something wrong. It was just a good example for the different implications of the code we write.
Which of course is getting off topic from the original blog post. But that's okay with me as long as I earn more kinky points. :)
fn()
fn( to )
fn( from, to )
This gets into method overloading, which is one of CF's limitations. Using method overloading in other languages, you'd be able to define two separate functions with the same name within a class to handle fn(to) and fn(from, to). CF just isn't built that way (yet).
It's been so long since I've done any software programming that sometimes I forget about stuff like method overloading.
@Russ S.,
Thanks Russ. I forgot about method overloading too.
@Steve, @Russ, @Allen,
Yeah, when we think of method overloading in languages like Java (not that I am a Java developer), it seems perfectly reasonable as they are two physically different methods.
It looks like for dynamic, non-sequential orders, we're all leaning towards named arguments. I am cool with that.
Call me old fashioned, but I would rather have my code work regardless of whether it was called as ordered arguments or named arguments. So... how about a simple check to see if an argument has a name and handle things differently if it does? Also, again maybe old school, but rather than figuring out how to hack in CFs argument type checking, I'd just check the type myself and throw. My old school code below. :)
<cfset var from = 0/>
<cfset var to = 1/>
<cfif arrayLen( arguments ) eq 1>
<cfset to = arguments[1]/>
<cfelseif arrayLen( arguments ) eq 2>
<cfif isdefined("arguments.from")>
<!--- using named args --->
<cfset from = arguments.from/>
<cfset to = arguments.to/>
<cfelse>
<!--- using ordered args --->
<cfset from = arguments[1]/>
<cfset to = arguments[2]/>
</cfif>
</cfif>
<cfif not isnumeric(to) or not isnumeric(from)>
<cfthrow message="An argument passed to the getRandomNumber function is not of type numeric."/>
</cfif>
<cfreturn randRange( from, to ) />
I'm sure there are downsides to this method (like CFC docs won't know the arguments needed), but that's the most elegant way I can think of at the moment. You could create a method for each permutation and then have that method maintain the argument definition and type checking, but I don't see that as more elegant. This method also scales to having the order change based on any number of attributes.
Just a thought,
Jason
@Jason,
I don't think there's anything wrong with this approach; having had some time to reflect on this whole post, I think the biggest issue with it is that the optional arguments are NOT in order.
Ben, today was the first time I've tried to use more than one optional argument in a function and I was just stumped (I come from a c++ background). Again, as always, you come through with the answer. Elegant solutions, elegant explanations. Thank you so much!