Calling ColdFusion Function Literals Like You Do In Javascript
One of the greatest things in Javascript is the use of the "headless" or "anonymous" function. You can define a function on the fly and pass it in as a method argument:
// Replace a value in a text field.
strText.replace(
new RegExp( "[0-9]", "g" ),
function( $0 ){
return( "blam" )
}
);
This Javascript replace method takes a headless function and uses it to evaluate each replace event.
After reading Sean Corfield's Closure Example I was inspired to mess with variable binding and function literals. To be honest, I still don't quite get closures. I think I have some mental block. maybe it's the "call()" method. What's that call() method all about? Can't you just execute a method directly? Clearly I am missing something.
So anyway, I went ahead and tried to create something like the headless Javascript method (above) but in ColdFusion. My example here takes text, a regular expression for phone numbers, and changes their format based on the function literal:
<!--- Store some text. --->
<cfsavecontent variable="strText">
For a good time, give Cindy a call at
212-555-1245. But, if you are feeling especially
naughty, try calling Betty at 555.5534.
</cfsavecontent>
<!---
Replace the phone number formatting using
our passed in function.
--->
<cfset strNewText = REReplaceWithMethod(
Text = strText.Trim(),
RegEx = "(?:(\d{3})[ .-])?(\d{3})[ .-](\d{4})",
Method =
"function( $0, $1, $2, $3 ){
if (Len( $1 )){
return( '(##$1##) ##$2##-##$3##' );
} else {
return( '##$2##-##$3##' );
}
}"
) />
As you can see, I am searching for an optional three digits followed by 3 and 4 more digits (with various delimiters). Notice that the because of the optional leading group, I have to have an IF statement in my passed in method. The function must be passed in as a string and must have named arguments. Running that code gives me:
For a good time, give Cindy a call at
(212) 555-1245. But, if you are feeling especially
naughty, try calling Betty at 555-5534.
It worked perfectly! Ok, so here's how it is done:
<cffunction
name="REReplaceWithMethod"
access="public"
returntype="string"
output="false"
hint="This takes a string, a regular expression, and a method against which each matching will be applied for the given replace.">
<!--- Define arguments. --->
<cfargument
name="Text"
type="string"
required="true"
hint="This is the text value we are going to manipulate."
/>
<cfargument
name="RegEx"
type="string"
required="true"
hint="This is our JAVA regular exiression."
/>
<cfargument
name="Method"
type="string"
required="true"
hint="This is our function string literal that we will we use to evaluate each match."
/>
<!--- Define the local scope. --->
<cfset var LOCAL = StructNew() />
<!---
Create a UUID for our call back method. We are using
UUID to help ensure that are given file name will
not conflict with an existing file.
--->
<cfset LOCAL.MethodID = (
"$" & CreateUUID().ReplaceAll( "[^\d\w]", "" )
) />
<!---
Create a file path for the actual ColdFusion method.
We are going to write this to the same directory
this template is in.
--->
<cfset LOCAL.FilePath = (
GetDirectoryFromPath( GetCurrentTemplatePath() ) &
LOCAL.MethodID
) />
<!---
Write method to disk. Since the function literal
is passed in a string, we need to surround it with
CFScript tags. Additionally, as the method is passed
in without a name, we need to name it using our
generated method ID.
--->
<cffile
action="WRITE"
file="#LOCAL.FilePath#"
output="<cfscript>#ReplaceNoCase( ARGUMENTS.Method, 'function(', 'function #LOCAL.MethodID#(', 'ONE' )#</cfscript>"
/>
<!---
Include the file. Since we are in a free-floating UDF,
this new method is going to be stored in the current
VARIABLES scope.
--->
<cfinclude template="./#LOCAL.MethodID#" />
<!---
Delete the file. The UDF has been loading into
ColdFusion memory. We no longer need the file.
THIS IS NOT EFFICIENT!
--->
<cffile
action="DELETE"
file="#LOCAL.FilePath#"
/>
<!---
Get a pointer to the method. The function named with
our given method iD has been stored in the calling
page's VARIABLES scope.
--->
<cfset LOCAL.Method = VARIABLES[ LOCAL.MethodID ] />
<!---
Get the method parameters. We are going to be calling
this method using CFInvoke so we need to know what the
arguments are called. This requires the use of NAMED
arguments.
--->
<cfset LOCAL.MethodParams = GetMetaData( LOCAL.Method ).Parameters />
<!---
Create a Java Patterns object and compile the passed
in regular expression.
--->
<cfset LOCAL.Pattern = CreateObject(
"java",
"java.util.regex.Pattern"
).Compile(
ARGUMENTS.RegEx
) />
<!--- Get the matcher. --->
<cfset LOCAL.Matcher = LOCAL.Pattern.Matcher(
ARGUMENTS.Text
) />
<!--- Create a string buffer for results. --->
<cfset LOCAL.Buffer = CreateObject( "java", "java.lang.StringBuffer" ).Init( "" ) />
<!---
Keep looping over the string finding matches for
our regular expression.
--->
<cfloop condition="LOCAL.Matcher.Find()">
<!---
For each match, we are going to pass all the
matching group to the passed in method. We have to
do this using CFINvoke and CFInvokeArgument and
therefore require named arguments.
--->
<cfinvoke
method="#LOCAL.MethodID#"
returnvariable="LOCAL.GroupResult">
<!--- Loop over the groups that we matched. --->
<cfloop
index="LOCAL.GroupIndex"
from="0"
to="#LOCAL.Matcher.GroupCount()#">
<!---
Get the value of the matched group. If the
regular expression has any optional groups,
it is possible that some of the Group()
calls will return a NULL value. Therefore,
it is possible that our GroupValue variable
will be destroyed.
--->
<cfset LOCAL.GroupValue = LOCAL.Matcher.Group(
JavaCast( 'int', LOCAL.GroupIndex )
) />
<!---
Check to see if we have any arguments in our
method left to use. If the RegEx and the
method literal are not quite aligned, we
might have too few available arguments.
--->
<cfif (LOCAL.GroupIndex LT ArrayLen( LOCAL.MethodParams ))>
<!---
Check to see if we have a value. If we
do not, then we are just going to send
over the empty string. This will help to
avoid some ColdFusion errors.
--->
<cfif StructKeyExists( LOCAL, "GroupValue" )>
<!--- Send over the group argument. --->
<cfinvokeargument
name="#LOCAL.MethodParams[ LOCAL.GroupIndex + 1 ].Name#"
value="#LOCAL.GroupValue#"
/>
<cfelse>
<!---
Since the group was not matched,
send over the empty string.
--->
<cfinvokeargument
name="#LOCAL.MethodParams[ LOCAL.GroupIndex + 1 ].Name#"
value=""
/>
</cfif>
</cfif>
</cfloop>
</cfinvoke>
<!---
Now that we have returned a value from the
passed in method, we can append this replacement
to the results buffer. We have to escape
RegEx-type characters as they will be evaluated.
--->
<cfset LOCAL.Matcher.AppendReplacement(
LOCAL.Buffer,
LOCAL.GroupResult.ReplaceAll( "([\$\\])", "\\$1" )
) />
</cfloop>
<!--- Append the remaining tail to the buffer. --->
<cfset LOCAL.Matcher.AppendTail(
LOCAL.Buffer
) />
<!---
Return the value, converting our running results
buffer into a string.
--->
<cfreturn LOCAL.Buffer.ToString() />
</cffunction>
Take a look at the variable binding (the stuff inside of the CFInvoke). This was the trickiest part. I am sure that this can be done in a much cleaner way, but I am at a loss as to how to do it. Anyway, I thought this was kind of a cool experiment. I wish this stuff was as easy and awesome as it was in Javascript, but I understand that due to compiling nature of ColdFusion, it just cannot be.
Want to use code from this post? Check out the license.
Reader Comments
Part of the problem here is that you are sort of confusing two levels of binding. There's the basic variable binding and then there's the pattern matching binding (of $n to parts of the matched string). That makes the problem doubly complicated.
With Closures for CFMX, you can certainly have anonymous arguments - you access them positionally in the closure code using arguments[n].
However, the closest idiomatic CF usage to what you show would be to have a two-argument closure that you pass the text string and the array result of the REFind() call into:
method = cf.new("
if (matches.pos[2] neq 0)
return '(' & mid(text,matches.pos[2],matches.len[2]) & ') ' &
mid(text,matches.pos[3],matches.len[3]) & '-' &
mid(text,matches.pos[4],matches.len[4]);
else
return mid(text,matches.pos[3],matches.len[3]) & '-' &
mid(text,matches.pos[4],matches.len[4]);",
"text,matches");
You can't just pass in the strings because mid() cannot take zero as an argument (if matches.pos[2] == zero).
Your REReplaceWithClosure() method would loop over the string, calling REFind() and then passing the array into a call of the closure:
subst = closure.call(result,matches);
result = left(result,matches.pos[1]-1) & subst & right(result,len(result)-matches.pos[1]+1-matches.len[1]);
(and then calling REFind() again starting at a new position).
You have to explicitly "call" the closure because it is not just a function, it is an object that has bound variables. Again, your example has no bound variables so you're not leveraging the power of closures.
Remember: a closure is not "just" an anonymous function, in the same way that in Java, an anonymous inner class is not synonymous with a closure either.
@Sean,
Thanks for taking the time to respond. I think I realize now that I know even less about closures than I realized I did :) I sort of see what you are saying, but I think I have to really go pick apart your Closure code to try and understand better.
I understand being able to access variable length arguments via ARGUMENTS[ n ]. But what I cannot figure out is how to invoke a method and pass a variable number of arguments.
I thought maybe I could build an ArgumentCollection object as an array, but I don't think it was happy with that. I can't use CFInvoke since that requires name/value pairs. Do you have any suggestions?
I will try an download and pick apart your code this weekend.
@Sean:
Can you offer a practical example of when a closure might be preferable to a more "traditional" approach? I'm with Ben, I think, in that closures tend to baffle me a bit, but maybe that's because I can't wrap my mind around a practical application for them. I also read your post this morning, but it didn't help me much (the darkness is just that thick).
@Ben,
closure.call(argumentCollection = someStruct);
closure.call(arg1);
closure.call(arg1,arg2);
Those are all valid calls, it's all a matter of what args you want to pass (maybe I'm not understanding what you're asking).
You could of course use cfinvoke with closures (method="call") to loop over cfinvokeargument tags.
@Rob, I'll post a few more examples on my blog in due course. Hopefully I can find something simple enough that folks can follow but meaty enough that folks see why a "traditional" approach would be much more work.
@Sean,
Sorry, I am not explaining myself well. Let's say I have an ultra simple function:
<cffunction name="Debug">
<cfdump var="#ARGUMENTS#" />
<cfabort />
</cffunction>
Now, this method does not have named arguments, nor is it limited by any length of arguments. What I was curious is, is there a way to build an "argumentCollection" for this type of a method.
Something like this, but this won't work (fails on the CFInvokeArugment I think):
<cfinvoke method="Debug">
<cfif true>
<cfinvokeargument value="TestA" />
</cfif>
<cfif false>
<cfinvokeargument value="TestB" />
</cfif>
<cfif true>
<cfinvokeargument value="TestV" />
</cfif>
</cfinvoke>
... I want to be able to send a variable-length argument list to a function that has no named arguments.
This could be completely crazy, just curious.
The only thing I can think of is to have a struct with numeric keys (since argumentCollection must be a struct)...
Oooooh! I think that might just do it. Gotta love the Array/Struct object that is ARGUMENTS.
Thanks!
Not sure what you ultimately did to solve this, but I thought of few things.
First, it seems a little odd that you would want a function that can take a variable number of arguments and not know the names of the arguments. Though obviously this being coldfusion overriding functions is done in the non-normal way of isdefined() within the function. It still seems odd to me, rather than having a single array variable which can have one or more elements.
However.
Arguments sent to a cffunction are ordered in arguments 1 through etc. You can do a structKeyArray( arguments ) and then do arrayLen to get length of arguments and use arguments[ num ] to grab the value. If the value happens to be a struct you can then have your own custom names for keys.