Skip to main content
Ben Nadel at BFusion / BFLEX 2010 (Bloomington, Indiana) with: John Farrar and Timothy Farrar
Ben Nadel at BFusion / BFLEX 2010 (Bloomington, Indiana) with: John Farrar ( @sosensible ) Timothy Farrar

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

By on
Tags:

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.

Want to use code from this post? Check out the license.

Reader Comments

426 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?

15,640 Comments

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

426 Comments

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?

15,640 Comments

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

426 Comments
		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!
426 Comments

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 />" );
15,640 Comments

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

426 Comments

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

426 Comments

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

426 Comments

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

15,640 Comments

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

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