Skip to main content
Ben Nadel at Scotch On The Rock (SOTR) 2010 (Munich) with: Michael Hnat
Ben Nadel at Scotch On The Rock (SOTR) 2010 (Munich) with: Michael Hnat

Calling ColdFusion Function Literals Like You Do In Javascript

By
Published in Comments (8)

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

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

15,848 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.

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

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

15,848 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.

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.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel