Invoking A ColdFusion Function With A Closure-Like CALLER-Based Variable Binding
The more I work with Javascript and jQuery, the more I love the power of closures. And, the more I love the power of closures, the more I want them in ColdFusion. As such, every time that I learn something new about the internal workings of ColdFusion, I stop and think about how I can leverage it to create closure-like behavior in my ColdFusion code. Recently, when I was playing around with Groovy, I figured out how to invoke ColdFusion component methods from within a Groovy context. Two years ago, the master-blaster, Elliott Sprehn, taught me how to get the PageContext from a ColdFusion custom tag CALLER scope. This morning, I woke up and suddenly wondered if I could combine these two features into a closure-like behavior?
If you look at my exploration of executing ColdFusion components in a Groovy context, you'll see that the key to invoking ColdFusion methods, using the underlying Java, is to have the appropriate Instance on which the method is being invoked as well as the appropriate parent Page to which the method is bound (NOTE: I don't fully understand what I just said - it's simply what I could piece together as an unfrozen caveman programmer). If we think about a ColdFusion custom tag context and its calling page, we can easily get the calling page instance (caller.variables); and, with what Elliott Sprehn has taught us, we can also get the calling page's context. As such, we should have everything we need in order to invoke a method from within a custom tag such that it is dynamically bound to the calling page context.
To demonstrate this, I have created a ColdFusion page with a user defined method, sayHello(). The sayHello() method returns a greeting that references a variables-scoped Name value. To demonstrate the dynamic binding of this UDF, I am going to pass it into a coldFusion custom tag and have the custom tag execute it in the calling context (where it will pick up the proper Name value).
<cffunction
name="sayHello"
access="public"
returntype="string"
output="false"
hint="I say hello to the NAME values stored in this template's VARIABLES scope.">
<!--- Define arguments. --->
<cfargument
name="adjective"
type="string"
required="false"
default="studly"
hint="I am an adjective to add to the greeting."
/>
<!--- Say hello to NAME variable. --->
<cfreturn "Hello #arguments.adjective# #name#!" />
</cffunction>
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!---
Define the name value to be used in SayHello() method. Take
note of this value as there will be a different NAME value in
the following custom tag (for binding demonstartion).
--->
<cfset name = "Tricia" />
<!---
Execute our test custom tag and pass in a reference
to the function we want to execut.
--->
<cf_contexttag
greeting="#sayHello#"
/>
As you can see, the sayHello() method makes uses of the unscoped "Name" variable, which it will look for in the binding page's Variables scope. The SayHello() method reference then gets passed into the ColdFusion custom tag, contexttag.cfm, which will execute the method using both the calling context as well as the tag-local context (for comparison).
ContextTag.cfm
<!---
Param the greeting attribute. This is the reference to
the SayHello() function defined in the calling context.
--->
<cfparam
name="attributes.greeting"
type="any"
/>
<!---
Now that the Greeeting function (SayHello()) is in the
context of this custom tag, if we execute it, it will
dynamically bind to the custom tag context (includ any
VARIABLES references). As such, we need to do some crazy
fenagaling (thank you Elliott Sprehn!) to get it to execute
in the context of the CALLER.
--->
<!---
Get the meta data for the caller page. This will give us the
CLASS used for the caller scope; from this, we can use
reflection to access the given field (page context) from
a given instance of that class (caller).
--->
<cfset callerMetaData = getMetaData( caller ) />
<!---
Get the class field that would hold a reference to the
pageContext.
--->
<cfset contextField = callerMetaData.getDeclaredField( "pageContext" ) />
<!---
This is a private field so we have to explicitly change its
access (I don't fully understand things at this level - this
is just what Elliott had... and, if you don't do it, it tells
you that the field is private and throws an error).
--->
<cfset contextField.setAccessible( true ) />
<!---
Now that we have the Field object that represents the
PageContext property, use reflection to get that property from
the CALLER instance.
--->
<cfset callerPageContext = contextField.get( caller ) />
<!---
This method will simply echo whatever arguments have
been passed to it. This is just an easy way to define an
argument collection to be used when invoking the passed-in
Greeting method.
NOTE: To be used with NAMED attributes. Positional attributes
have be executed using an Object[] array... (I think).
--->
<cffunction
name="getArgumentCollection"
access="public"
output="false"
hint="I echo the arguments collection.">
<cfreturn arguments />
</cffunction>
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!---
At this point, we have everything we need to invoke the given
method in the context of the calling page:
* Local scope reference (Variables).
* Calling page context.
* Argument collectoin object.
--->
<!---
To demonstrate that the method is executing in the right
context, let's create a tag-local NAME variable.
--->
<cfset name = "Joanna" />
<!---
Execute the gretting method in the context of the calling page
and store the result.
NOTE: To invoke the method, we are using the underlying,
undocumented Java methods for invoking ColdFusion methods.
--->
<cfset result = attributes.greeting.invoke(
caller.variables,
javaCast( "string", getMetaData( attributes.greeting ).name ),
callerPageContext.getPage(),
getArgumentCollection( adjective = "coolest" )
) />
<cfoutput>
Caller-Context: #result#<br />
<br />
<!---
To demonstrate that this is doing some MAGIC, let's also
execute the UDF in the context of teh custom tag to see
which NAME variable it binds to.
--->
Tag-Context: #attributes.greeting()#<br />
</cfoutput>
<!--- Exit out of the custom tag. --->
<cfexit method="exittag" />
As you can see in the above code, first we get the calling page's context. Then, we define a local Name variable, "Joanna", and execute the passed-in method, binding it once to the calling page context and then once to the custom tag context. When we run the above code, we get the following output:
Caller-Context: Hello coolest Tricia!
Tag-Context: Hello studly Joanna!
As you can see, when the sayHello() method is invoked on the calling context, it picks up, "Tricia," as the Name variable value; and, when it's invoked on the ColdFusion custom tag context, it picks up, "Joanna," as the Name variable value. Isn't that wild?!? Furthermore, when we invoke the method on the calling context, we also pass in the optional argument, Adjective, "coolest."
ColdFusion custom tags give us the CALLER scope, which is what makes this possible. From everything that I have seen so far, no such calling-page-link exists in any other complex construct within ColdFusion (components, methods) - at least not that I have been able to find in any of the pageContext-accessible data. This whole exercise might just seem like a whole bunch of silliness, but I do have a follow up blog post that might make is seem pretty cool.
Want to use code from this post? Check out the license.
Reader Comments
I know about as much ColdFusion as a mitochondria, but I wonder that this looks like standard OOP. The scope of your variables remain in effect until and unless you trash them or step outside of the closure. Is this something not normally found in ColdFusion? Did I miss something....?
@Kristopher,
It doesn't quite work that way with unbound methods (unbound to a ColdFusion component at any rate). The way ColdFusion works is that when you execute an unbound method, it dynamically binds to the page object associated with the execution context; this renders the context in which the method was defined as useless (relatively speaking).
This is great because it allows a high degree of dynamic behavior in the language. But, sometimes, it would be nice to have the define-time binding that closures that offer.
Hopefully, I'll demo this in my next blog post.
Here is the follow up to this post, which demonstrates how to use this approach with regular expression match replacement:
www.bennadel.com/blog/1814-Using-A-ColdFusion-Method-Closure-In-Regular-Expression-Replace-Logic.htm
@Ben
Closures either have to be a language feature or have to be implemented in code without tricks. A sample usage of a sample API for closures implemented in code is as follows. The implementation of the API is left as an exercise to the reader.
function f(a, b, c) {
//a, b, and c are in the arguments scope
//d, e, and f are in the variables scope
return a + b + c + d + e + f;
}
//the closure parameter becomes the
// variables scope of the function,
// at the time the function is called
// via the delegate's call method
fd = new Delegate(f, closure = { d = 'x', e = 'y', f = 'z' });
//call the function via the delegate's
// call method, passing in the arguments
// (you can use either array or struct arguments)
//should print out '123xyz'.
writeOutput(fd.call('1', '2', '3'));
Cheers,
Justice
@Justice,
Right - Unfortunately ColdFusion doesn't have support for Closures, nor does it make it easy to simulate (ala my example). I am not sure how the Delegate would work? It seems like there would be a lot of overhead to making sure that the closure scope creates two-way communication between variable read/creation. Meaning, a closure method can set values INTO the closing scope as well as just reading from them.
@Ben,
Right. So you would simply have to implement closures the same way any other language with closures implements them behind the scenes.
Rather than using the caller scope, you can create a closure out of a non-closure function and a struct with the variables to be closed over.
Cheers,
Justice
@Justice,
Absolutely! And I hope they do (figure out how to do it). This was mostly an experiment in trying to push the language in its current state.