Skip to main content
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Ryan Anklam
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Ryan Anklam@bittersweetryan )

Using Closures To Bind Naked Functions To Components In ColdFusion

By on
Tags:

When we think about a "method signature", we often think solely about the arguments that it accepts and the type of data that it returns. But, there's more to a method signature, such as the mode in which it can be invoked. Most methods can only be invoked as a member method. However, in some cases, an Object's API allows for methods to be detached and passed-around as "naked functions". In ColdFusion, we can use Closures / Lambdas to bind a Function reference to a ColdFusion Component instance such that the "member method" can be used - and work correctly - as a "naked function".

Allowing component member methods to be used (successfully) as naked function references is a deliberate choice in the component architecture; and, requires the code to be wired-up slightly differently. But, it allows for some very elegant solutions. For example, Angular's $q methods can be passed around as naked functions, which allows them to be used as callbacks:

setTimeout( deferred.resolve, 1000 );

And, Chalk's style methods can be passed around as naked functions, which allows us to build-up and cache complex style variations:

var asError = chalk.bold.red;

In ColdFusion, a Closure "closes over" the lexical this and variables scope of the defining context. Which means, if we define a closure / lambda function inside a ColdFusion component, anything that we execute inside that closure will be executed in the context of the component's this and variables scopes.

If the "thing" that we're executing inside one of these closures is a Function invocation, that Function invocation will be implicitly provided with the lexically-bound this and variables scopes. Which means, that "Function" will act like a "member method" on that component:

component {

	/**
	* I return a CLOSURE / LAMBDA function that binds the given method to this component.
	* This allows the resultant function to be passed-around as a "naked function" without
	* breaking its connection to THIS component context.
	*/
	public function function bindMethod( required function method ) {

		var proxyMethod = () => {

			// This proxyMethod() closure retains the LEXICAL THIS and VARIABLES scopes.
			// Which means that when the following method() call below takes place, it
			// takes place in the context of these lexically-bound scopes - ie, in the
			// context of this Component.
			return( method( argumentCollection = arguments ) );

		};

		return( proxyMethod );

	}

}

Inside a ColdFusion component, we don't need to use this. or variables. scoping to invoke a member method - we can just use the name of the method itself and ColdFusion will automatically invoke it in the component context. As such, in the above snippet, we can use method() as an unscoped invocation and ColdFusion will bind it to the "current context". And, since the proxyMethod() closure "closes over" the this and variables scope, the given method() invocation will act like a normal, unscoped member method on the component.

It's pretty cool what degree of power a few lines of ColdFusion code gives us; but, then again, that's just the kind of thing ColdFusion developers have come to expect from the platform.

To see this in action, let's create a Queue component with push and pop methods that can be detached from the queue instance and still work properly as "naked functions". As I mentioned above, adding this characteristic to the method signatures requires a slight change in architecture. In this case, within the component constructor, we have to override the method bindings to use the bindMethod() function from above:

component
	output = false
	hint = "I hold an internal First-in-last-out (FILO) queue of items."
	{

	/**
	* I initialize the FILO queue with the given name.
	*/
	public void function init( string name = "UnnamedQueue" ) {

		this.name = name;
		variables.items = [];

		// Bind the public methods to this component instance. This will convert the given
		// methods into Lambda Functions that have a LEXICAL binding to both the THIS and
		// VARIABLES scopes of this component. This will allow them to be passed-around as
		// "naked functions" without losing their invocation bindings.
		this.pop = bindMethod( this.pop );
		this.push = bindMethod( this.push );
		this.info = bindMethod( this.info );
		this.toArray = bindMethod( this.toArray );

	}

	// ---
	// PUBLIC METHODS.
	// ---

	/**
	* I return a CLOSURE / LAMBDA function that binds the given method to this component.
	* This allows the resultant function to be passed-around as a "naked function" without
	* breaking its connection to THIS component context.
	*/
	public function function bindMethod( required function method ) {

		var proxyMethod = () => {

			// This proxyMethod() closure retains the LEXICAL THIS and VARIABLES scopes.
			// Which means that when the following method() call below takes place, it
			// takes place in the context of these lexically-bound scopes - ie, in the
			// context of this Component.
			return( method( argumentCollection = arguments ) );

		};

		return( proxyMethod );

	}


	/**
	* I get meta information about the FILO queue.
	*/
	public struct function info() {

		return({
			name: this.name,
			itemCount: variables.items.len()
		});

	}


	/**
	* I pop the most recent item off of the queue and return the item.
	*/
	public any function pop() {

		if ( ! items.isDefined( 1 ) ) {

			return;

		}

		return( items.shift() );

	}


	/**
	* VARIADIC METHOD: I push the given values onto the FILO queue.
	*/
	public any function push( required any value ) {

		for ( var i = 1 ; i <= arrayLen( arguments ) ; i++ ) {

			items.prepend( arguments[ i ] );

		}

		return( this );

	}


	/**
	* I get a shallow-copy of the internal queue of items.
	*/
	public array function toArray() {

		return( arrayNew( 1 ).append( items, true ) );

	}

}

The key part of this entire ColdFusion component demo is the block of code in the constructor that is taking member methods and is overwriting them with the proxy / bound version of that method, example:

this.pop = bindMethod( this.pop );

Now, when an external context invokes the .pop() method, they are not actually executing the compile-time method - they are invoke the overwritten lambda which has bound the member method to the lexical this and variables scopes.

To see this in action, let's instantiate Queue.cfc, pluck off the member methods, and then try to use them as naked functions:

<cfscript>

	q = new Queue( "NakedFnQueue" );

	// Let's store queue method references as "naked functions". Meaning, let's store them
	// so that they are not invoked as component methods, but as a free-standing Function
	// reference that can be passed-around.
	push = q.push;
	pop = q.pop;
	info = q.info;
	toArray = q.toArray;

	writeOutput( "<h2> Member Method Test </h2>" );

	// CONTROl: Make sure that the methods work as part of a standard invocation.
	q.push( "Control Test" );
	writeDump( q.info() );
	writeDump( q.pop() ); // Pop "Control Test".

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	writeOutput( "<h2> Naked Function Test </h2>" );

	// Now, let's try to test these naked functions!
	push( "A", "B", "C" )
		.push( "D" )
		.push( "E" )
		.push( "F" )
	;
	writeDump( pop() ); // Pop "F".
	writeDump( pop() ); // Pop "E".
	writeDump( pop() ); // Pop "D".
	writeDump( info() );
	writeDump( toArray() );

</cfscript>

As you can see, we're storing naked function references for push(), pop(), info(), and toArray(); and, we're using them to affect the state of the original component instance. And, when we run this ColdFusion code, we get the following output:

State of the queue demonstrates that naked function invocation changed the state of the original parent component.

As you can see, the lexically-bound member methods that we plucked off of the Queue.cfc instance were able to read-from and act-upon the state of the original ColdFusion component instance.

Now, since the bindMethod() is a public method on the Queue.cfc ColdFusion component, it means that we can pass other methods into it - methods that weren't members of the original component definition. In the following demo, we're going to generate an array iterator that pushes each array element as a transformed value onto the queue:

<cfscript>

	version = ( server.keyExists( "lucee" ) )
		? "Lucee CFML #server.lucee.version#"
		: "Adobe ColdFusion #server.coldfusion.productVersion#"
	;

	writeOutput( "<h1> #version# </h1>" );

	q = new Queue( "NakedFnQueue" );

	// Bind our mapPush() method to the Queue instance so that it will execute as if it
	// were a member method of the component.
	// --
	// CAUTION: This has to be a FUNCTION DECLARATION, not a closure so that mapPush()
	// isn't lexically bound to its own page context.
	operator = q.bindMethod( mapPush );

	// Now, use the operator() function as the Array Iterator, which will cause each item
	// in the array to be passed to the proxied version of the Queue push() method.
	arrayEach( [ "Osh", "Kosh", "B'gosh" ], operator );

	writeDump( q.toArray() );

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	/**
	* I wrap the core push() method and transform the pushed value.
	* 
	* CAUTION: This is a FUNCTION DECLARATION, not a CLOSURE. This is critical because a
	* closure would "close over" the lexical THIS and VARIABLES scope, breaking the call
	* to push() internally.
	*/
	public void function mapPush( required any value ) {

		push( "This is my mapped value: #value#" );

	}

</cfscript>

Here, our mapPush() function declaration is being used as the Array iterator and, for each iteration, it is pushing transformed values onto the queue. When we run this ColdFusion code in Lucee CFML (or Adobe ColdFusion 2021), we get the following output:

State of the queue altered by mapPush() function that was bound the queue component context.

In this demo, I'm stressing the fact that mapPush() is a function declaration, not a closure. This is because the demo would break if it were a closure. A closure would have "closed over" its own variables scope and page context, which means that the internal call to push() would have failed.

If you want to use a closure in this example, you don't even need the bindMethod() approach because you an just "close over" the original reference to the queue component itself:

<cfscript>

	q = new Queue( "NakedFnQueue" );

	operator = ( value ) => {

		// NOTE: We're using the lexically-bound reference to "q" and we are simply
		// calling it's member method, .push() - nothing fancy going on here.
		q.push( "This is my mapped value: #value#" );

	};

	arrayEach( [ "Osh", "Kosh", "B'gosh" ], operator );

	writeDump( q.toArray() );

</cfscript>

Here, since we're defining operator() as a closure, we can reference q internally and receive the lexically-bound instance of the Queue.cfc. In that case, we don't even really care about the "naked functions" concept because we can just invoke .push() as a member method.

To be fair, I use this technique much more in JavaScript than I do in ColdFusion since the vast majority of my ColdFusion components are single-instances that are cached in memory; and, I think this technique works best for transient objects (like the JavaScript Promise). That said, if nothing else, this demonstrates the power of closures in ColdFusion; and hopefully sheds light on some interesting techniques.

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

Reader Comments

Post A Comment — I'd Love To Hear From You!

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.