Calling ColdFusion Function Literals Like You Do In Javascript

Posted March 16, 2007 at 2:17 PM by Ben Nadel

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

Mar 16, 2007 at 3:52 PM // reply »
120 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.


Mar 16, 2007 at 4:13 PM // reply »
11,238 Comments

@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.


Mar 16, 2007 at 4:17 PM // reply »
43 Comments

@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).


Mar 16, 2007 at 4:24 PM // reply »
120 Comments

@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.


Mar 16, 2007 at 4:41 PM // reply »
11,238 Comments

@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.


Mar 16, 2007 at 4:56 PM // reply »
120 Comments

The only thing I can think of is to have a struct with numeric keys (since argumentCollection must be a struct)...


Mar 16, 2007 at 5:00 PM // reply »
11,238 Comments

Oooooh! I think that might just do it. Gotta love the Array/Struct object that is ARGUMENTS.

Thanks!


Jul 16, 2007 at 5:51 AM // reply »
1 Comments

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.


Post A Comment

Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 17, 2013 at 7:42 PM
HashKeyCopier - An AngularJS Utility Class For Merging Cached And Live Data
Ben - thanks so much for posting these Angular articles and findings, they've been a huge help towards learning one of the more 'complex' JavaScript frameworks out there (IMO). I have been using Angu ... read »
May 16, 2013 at 5:01 PM
UPDATE: Parsing CSV Data Files In ColdFusion With csvToArray()
Your code was the closest thing I've found to obtaining some direction for converting ISO fields to values that CF can translate properly. Thank you for posting! ... read »
May 15, 2013 at 10:37 PM
Very Simple Pusher And ColdFusion Powered Chat
hi id making plz easy ... read »
May 15, 2013 at 6:07 PM
Making SOAP Web Service Requests With ColdFusion And CFHTTP
Ben, you once again saved my bacon at work. Thank you, thank you, thank you! ... read »
May 15, 2013 at 4:15 PM
What If All User Interface (UI) Data Came In Reports?
@Josh, Thanks! @Ben, I definitely recommend the David West book "Object Thinking" I've been quoting from. It goes deeply into the philosophy and history of OO programming. His breadth ... read »
May 15, 2013 at 11:36 AM
Ask Ben: Print Part Of A Web Page With jQuery
I found this helpfull when you need to keep (refresh) the original parent page after closing the iframe child print dialog (Hoping you're not using a form at this time so it won't submit again): On ... read »
May 14, 2013 at 7:13 PM
What If All User Interface (UI) Data Came In Reports?
@Jonah, If there's any books you'd recommend on the subject of domain modelling, I'd love to hear it. I just downloaded the free PDF of "Domain Driven Design Quickly". Figured I'd give it ... read »
May 14, 2013 at 6:57 PM
The UX Of Prototyping: Low-Fidelity Is The New High-Fidelity
@Phillip, I'm not sure I follow what you mean? Are you saying that you looked at the list of widgets provided by the jQuery UI and let that be your style guide? ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools