Making ColdFusion Closures More Flexible With Context Arguments
Yesterday, after I released my BitBuffer.cfc ColdFusion component, I started to think about the transformBits() method. I was using a ColdFusion Closure to map one set of bits onto another. But, what if I wasn't using ColdFusion 10 (or higher)? Or, what if I simply defined my transform-operator as a normal ColdFusion function without closure scoping? This got me thinking about making closure more flexible by allowing an optional "context" argument to be passed-through.
The beauty and power of a ColdFusion closure (and closures in general) is that the body of the closure maintains references to values defined in the lexical scope. This can make life pretty awesome. But, if you aren't using a closure - if you're just using a normal function - the function can only reference values in the execution context, not the lexical context.
Now, this is by no means a novel idea (this is sort-of done in JavaScript all the time) but, it occurred to me that functional operators could be made more flexible by allowing for an optional "context" argument. This context argument would provide a tunnel through which arbitrary data could be passed. Then, you could define closure-based operators without the context and non-closure-based operators with the context.
To see what I mean, I've defined a map() function that maps one collection onto another using a function operator. The last argument of the operator is an optional context which gets passed-in during operator execution. Then, I consume the map() function using both a closure-based operator as well as a normal ColdFusion function operator:
<cfscript>
/**
* I map the given collection onto a resultant collection using the given iteration
* opeator. If the operator does not return a value, the input is removed from the
* resultant collection.
*
* @collection I am the input collection being mapped onto an output collection.
* @operator I am a ColdFusion function or ColdFusion closure.
* @context I am an optional object that is passed to the operator.
* @output false
*/
public array function map(
required array collection,
required any operator,
any context = structNew()
) {
var result = [];
for ( var i = 1 ; i <= arrayLen( collection ) ; i++ ) {
// When invoking the operator, pass the "context" object through as the
// last argument. This provides a tunnel through which arbitrary information
// can be passed when the user is NOT using a closure. This allows this
// function to be used by both older and newer versions of ColdFusion.
var mappedValue = operator( collection[ i ], i, collection, context );
// If a mappedValue was returned, add it to the result. Otherwise, discard
// the input value.
if ( structKeyExists( local, "mappedValue" ) ) {
arrayAppend( result, mappedValue );
}
}
return( result );
}
// ------------------------------------------------------ //
// ------------------------------------------------------ //
/**
* I map the given array onto a resultant array by removing the values that need
* to be ignored (for the sake of the demo).
*
* @collection I am the array being mapped.
* @ignoreValues I am the array of values to exclude from the mapping.
* @output false
*/
public array function filterWithClosure(
required array collection,
required array ignoreValues
) {
// NOTE: When we define our operator funciton, notice that we are leveraging the
// power of ColdFusion closures to reference the "ignoreValues" within the body
// of the operator.
var result = map(
collection,
function( item ) {
// Only return the current item if it is not meant to be
if ( ! arrayContains( ignoreValues, item ) ) {
return( item );
}
}
);
return( result );
}
/**
* I map the given array onto a resultant array by removing the values that need
* to be ignored (for the sake of the demo).
*
* @collection I am the array being mapped.
* @ignoreValues I am the array of values to exclude from the mapping.
* @output false
*/
public array function filterWithoutClosure(
required array collection,
required array ignoreValues
) {
// NOTE: When we define our operator funciton notice that we are NOT using a
// closure. Instead we are using a normal ColdFusion function and passing in
// the local-scope as the context.
var result = map( collection, nonClosureOperator, local );
return( result );
}
/**
* I am the non-Closure operator used to demonstrate that normal functions aren't
* necessarily restricited to pre-ColdFusion 10.
*/
public any function nonClosureOperator(
required any item,
required numeric index,
required array collection,
required any context
) {
if ( arrayContains( context.arguments.ignoreValues, item ) ) {
return;
}
return( item );
}
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// Setup our demo values.
values = [ 1, 2, 3, 4, 5 ];
evenValues = [ 2, 4 ];
// Filter the values with and without closure technology.
writeOutput( arrayToList( filterWithClosure( values, evenValues ) ) );
writeOutput( "<br />" );
writeOutput( arrayToList( filterWithoutClosure( values, evenValues ) ) );
</cfscript>
In the case where I'm not using a Closure, you can see that I am passing the calling context's Local scope through as the operator context argument. This gives my non-closure operator access to the local variables and the arguments scope of the calling context. And, when we run the above code, we get the following output:
1,3,5
1,3,5
As you can see, both approaches yield the same result.
Using this optional-context approach is nice because it means that, with a little extra effort, your code can be consumed in a wider range of conditions. Remember, the non-closure function isn't just some relic of a pre-ColdFusion 10 world - it's any "modern" function that isn't defined as a closure.
Want to use code from this post? Check out the license.
Reader Comments
@All,
I know this blog post title is not that accurate - I'm not really making *closures* more flexible - I'm making functional operators more flexible, which may or may not be defined as closures. Let's be honest here - I didn't know how to articulate that very well in a title :D
I now know a fair bit more about closures than I did before. it's a difficult concept for me in that I'm unsure what use cases are best served with closure. In other words, when would (or should) I say to myself... Closures would be perfect here! When that aha moment happens, I'm confident I'll "get it"
Ha! thanks Ben. I jumped right in here and then got confused. But now it makes sense. We're good!
@Chris, @Doug,
I don't have a great sense for where to use them in ColdFusion. I know that I love closures; but, since I am not yet on ColdFusion 10 in production (though hopefully shortly), I don't have battle-tested thoughts on it as of yet.
That said, in JavaScript, closures are used very heavily with things like the Revealing Module Pattern. That said, JavaScript doesn't have true public/private scoping like ColdFusion, so that pattern doesn't necessarily apply.
Mostly, I think they'll be used for functional programming where methods like .each() and .map() and .fold() will work on collections using closure-based operators.
It's a really cool technology, just takes a little mental gymnastics to get comfortable with.
@Ben,
YEah, I kinda feel the same way about closures, excited but sometimes a little confused. Your post gave me a new way of looking at it in CF (and I'm still only theoretically into 10 myself too.)