Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at CFUNITED 2008 (Washington, D.C.) with: Ken Auenson
Ben Nadel at CFUNITED 2008 (Washington, D.C.) with: Ken Auenson@KenAuenson )

Using invoke() To Invoke Passed-In Closure And Function References In ColdFusion

By Ben Nadel on
Tags: ColdFusion

This morning, I had a huge break-through in my understanding of the invoke() function in ColdFusion. The first argument of the invoke() function is documented as being either a ColdFusion Component instance (or Java or COM or Corba or .NET instance); or, the name of a ColdFusion Component class file. Or, it could be an empty string if you were just invoking a method in the current context. These documented constraints fall-down when you want to use invoke() to invoke a Closure or Function reference that was passed-in as an argument to the current context. Luckily, I just discovered that the first argument to invoke() can be the Arguments scope. Or, really, any other structure that contains the given function name.

To be fair, I actually stumbled on this feature 3-years ago when I realized that the invoke() function could be used to invoke methods on Java objects. But, I wasn't smart enough at the time to realize the larger implications of what I had found. Three years ago, I just equated "Java Object" to "ColdFusion Component" - I never made the connection that the Java Object in question was really just a "data sack" that contained the method.

NOTE: To be fair, using a Java object with invoke() is in the documentation; so, if you squint hard enough, it's reasonable to not extrapolate any further that the use of what was documented.

That said, the invoke() function is an important function in ColdFusion because it allows us to dynamically invoke a method based on its name, not its reference. But, it's also an important function because it allows the optional arguments to be provided as either an Array or a Struct.

To understand why that is helpful, let's take a quick look at the limitations of the "argumentCollection" feature of ColdFusion. The argumentCollection argument is kind of like the "spread operator" in other languages - it allows a collection of arguments to be applied to a function invocation. But, those arguments have to be supplied as a Struct. If we attempt to provide them as an array:

  • <cfscript>
  •  
  • public string function echoFunction( required string message ) {
  •  
  • return( message );
  •  
  • }
  •  
  • writeOutput( echoFunction( argumentCollection = [ "Hello world!" ] ) );
  •  
  • // NOTE: Technically, we could use a Struct to provided "ordered arguments" where
  • // each argument is keyed by its index in the function signature. This works, but has
  • // had some quirkiness in earlier versions of ColdFusion. And, it requires the a
  • // greater understanding of how the arguments collection is being gathered.
  • // --
  • // writeOutput( echoFunction( argumentCollection = { "1": "Hello world!" } ) );
  •  
  • </cfscript>

... we get the following ColdFusion error:

The MESSAGE parameter to the echoFunction function is required but was not passed in.

NOTE: You can use a Struct to define ordered arguments, dating back to the CFInvokeArgument. But, it has had some quirky behavior over the years.

Thankfully, the invoke() function in ColdFusion allows us to use the same kind of dynamic invocation while also allowing us to provide the argument collection as either an Array or a Struct (or an Arguments scope reference). Here's the same example, using invoke():

  • <cfscript>
  •  
  • public string function echoFunction( required string message ) {
  •  
  • return( message );
  •  
  • }
  •  
  • // Unlike the argumentCollection construct, the invoke() function can take either an
  • // Array or a Struct as the optional arguments argument.
  • writeOutput( invoke( "", "echoFunction", [ "Hello world!" ] ) );
  •  
  • </cfscript>

If we run this code, we get the following ColdFusion output:

Hello world!

As you can see, where the "argumentCollection" invocation falls down, the invoke() function works quite nicely.

Notice, however, that the first argument in our invoke() call is the empty string. This is because our "echoFunction" is not part of a ColdFusion Component, but rather just part of the current page context. If the given function is more localized than the page, such as it is when provided as a function argument, this use of invoke() breaks:

  • <cfscript>
  •  
  • public string function echoFunction( required string message ) {
  •  
  • return( message );
  •  
  • }
  •  
  • public string function proxy(
  • required function target,
  • required array targetArguments
  • ) {
  •  
  • return( invoke( "", "target", targetArguments ) );
  •  
  • }
  •  
  • writeOutput( proxy( echoFunction, [ "Hello world!" ] ) );
  •  
  • </cfscript>

As you can see with this example, the signature of the invoke() method is the same: it starts with an empty string and then takes the name of the argument that contains the passed-in function reference. Unfortunately, when we run this, we get the following ColdFusion error:

Entity has incorrect type for being called as a function. The symbol you provided target is not the name of a function.

In this case, ColdFusion doesn't understand that the function named, "target", is in the arguments scope, not the page scope. But, it turns out that all is solved if you simply provide the Arguments scope references as the "component reference" parameter of the invoke() function:

  • <cfscript>
  •  
  • /**
  • * This method will proxy the invocation of the given Function / Closure.
  • *
  • * @target I am the Function being proxied.
  • * @targetArguments I am the argument-collection being used during invocation.
  • * @output false
  • */
  • public any function proxy(
  • required function target,
  • required any targetArguments
  • ) {
  •  
  • // The documentation says that invoke() has to take the name of a component or
  • // a component reference. But, it seems that the constraint is really just the
  • // context on which the given named-function can be found. In this case, it is
  • // the Arguments scope; but, it could just have easily been the Local scope or
  • // some other Struct or Java object.
  • return( invoke( arguments, "target", targetArguments ) );
  •  
  • }
  •  
  • // ------------------------------------------------------------------------------- //
  • // ------------------------------------------------------------------------------- //
  •  
  • public string function echoFunction( required string message ) {
  •  
  • return( message );
  •  
  • }
  •  
  • echoClosure = function( required string message ) returnType = "string" {
  •  
  • return( message );
  •  
  • };
  •  
  • // ------------------------------------------------------------------------------- //
  • // ------------------------------------------------------------------------------- //
  •  
  • // With ordered arguments.
  • writeOutput( proxy( echoFunction, [ "Hello world from Function!" ] ) & "<br />" );
  • writeOutput( proxy( echoClosure, [ "Hello world from Closure!" ] ) & "<br />" );
  •  
  • // With named arguments.
  • writeOutput( proxy( echoFunction, { message: "Hello world from Function!" } ) & "<br />" );
  • writeOutput( proxy( echoClosure, { message: "Hello world from Closure!" } ) & "<br />" );
  •  
  • </cfscript>

As you can see, in this version, I'm providing the "arguments" scope reference as the first invoke() argument. This allows ColdFusion to find the "target" property on the arguments collection and successfully invoke the given function or closure reference. As such, when we run this code, we get the following ColdFusion output:

Hello world from Function!
Hello world from Closure!
Hello world from Function!
Hello world from Closure!

Woot woot! With an understanding that the first argument doesn't actually have to be a ColdFusion Component, Java, COM, CORBA, or .NET object (as documented), it means that we can pass in any-old Object reference (like the Arguments scope or Local scope). Then, use the second argument to point to a named-property on said object reference.

I feel somewhat foolish for not understanding this sooner. But, I suppose it's better late than never. Knowing that I can pass-in a scope-references as the first argument to the invoke() function will make it easy-peasy to invoke passed-in Closure and Function references when dealing with more abstract behaviors like database retry-logic and StatsD instrumentation.



Looking For A New Job?

Ooops, there are no jobs. Post one now for only $29 and own this real estate!

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader Comments

I am a little puzzled, how "target" manages to magically become a variable reference. It looks like a plain old string to me. I must be missing something?

Reply to this Comment

@Charles,

Think of the call:

invoke( arguments, "target", args )

... as being translated into:

arguments[ "target" ]( argumentCollection = args )

... it's not a perfect mental model. But, you can see that the "targets" value is just a "property" on the first argument, which in this case, is arguments.

Reply to this Comment

So you are saying that you can surround an argument Property in quotes and it acts just like any other property reference. That's cool. I never knew this.

Can you do the same for ordinary variables, just out of interest:

var "target" = "foo";

But, going back to the original issue, do you actually need the quotes, or are are you just using them for emphasis?

Reply to this Comment

@Charles,

I am not saying this in "general" -- only specifically when using the invoke() method. If you're outside of the invoke method, you still need to reference a variable like a variable. Though, as with other languages, like JavaScript, you can still reference string-based props on a scope. For example:

function myFunction( target ) {
	target(); // works - direct variable reference.
	arguments[ "target" ]; // works - named-property reference on Arguments scope.
	"target"; // FAILS(ish) - this is not a reference to the target arguments.
}

In the case of the invoke() we need to quote it because the method expects a String (which has to be the name of a method property).

Reply to this Comment
		required function target,
		required any targetArguments
		) { 
		return( invoke( arguments, "" & target & "", targetArguments ) );
}```
	
I can understand this working, but I am still amazed that your version works! I must try it out!
Reply to this Comment

Sorry, here is what I meant to write:

public any function proxy(
		required string target,
		required any targetArguments
		) { 
		return( invoke( arguments, "" & target & "", targetArguments ) );
}
writeOutput( proxy( "echoFunction", [ "Hello world from Function!" ] ) & "<br />" );
Reply to this Comment

@Charles,

Right, so I believe that your version will break. When you do & target &, ColdFusion is going to try to cast the Function reference to a String, which it won't be able to do. When I quote target, "target", in the invoke() I am telling it to look for an property named target in the arguments collection.

Reply to this Comment

@Ben,

OK. I am getting there. So, will 'invoke' only look in the arguments collection for this reference to a special quoted property? So, this is a special kind of association between 'invoke' & the parent function's arguments collection?

So, if you did not provide this 'proxy' 'target' argument, the 'invoke' "target" argument would just try to reference a function called target()? But, because it looks in the 'proxy' argument collection and finds 'target', it then resolves this to 'echoFunction' or 'echoClosure '?

This is totally fascinating. I have never seen this before...

Reply to this Comment

OK. Ben. I think I understand now.

The important bit is:

invoke( arguments,...

This tells 'invoke' that the context is the 'arguments' scope.
This is how it links the rest of the 'invoke' arguments back to the 'proxy' arguments.
Everything now makes sense, although the quoted "target" thing is very cool!
I have definitely learnt something new today. Thanks...

Reply to this Comment

Just one other thing about your blog in general. Is there any way you can add a 'delete' comment functionality. I wish I could have deleted some of my previous comments. I tend to be quite impetuous when I write comments.
It just means I could delete my idiotic comments, before you get to see them;)

Reply to this Comment

@Charles,

Yeah, there's no great way to delete comments. But, that's a good idea. That's something I should be able to put in the app with not too much effort (for some temp period of time, like 5-minutes to delete a comment after its posted).

Re: the quoting and resolving, part of the "trick" here is understanding the peculiarities of ColdFusion. Essentially each "page" in a ColdFusion application has an implicit variables scope. You can think of this as the "private scope" for that page. So, when you reference a value that is not explicitly scoped, ColdFusion will look in the variables scope, among other scopes, to try and resolve the reference.

So, in the first demo, where I have:

invoke( "", "echoFunction", [ "Hello world!" ] )

.... you can think of this as really being:

invoke( varaibles, "echoFunction", [ "Hello world!" ] )

.... where ColdFusion is implicitly looking in the variables scope because I am not providing any other scope. This is an over-simplification, because it probably looks in the this scope as well in the context of a Component. But, for the most part, ColdFusion has some set of scopes that will look at for variable resolution.

Though, I fear this may just be confusing the matter :)

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
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.