Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with:

Calling ColdFusion Function Literals Like You Do In Javascript

By Ben Nadel on
Tags: ColdFusion

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.




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.

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.