ColdFusion 10 Beta - Closures, Function Expressions, And Functional Programming
ColdFusion 10 Beta introduced two revolutionary concepts to the ColdFusion language: Closures and Function Expressions. Closures allow functions to retain their lexical bindings when passed out of context; Function Expressions allow us to define functions as part of larger expressions. These two new features really facilitate the use of "functional programming" - code that focuses on evaluating expressions and nesting functions. To continue our exploration of closures and function expressions in ColdFusion 10 beta, I thought I'd have some fun and translate a number of common "functional programming"-style functions into ColdFusion 10 script.
Before we look at any functions, let's take a look at how functional programming changes the way our code works. In the following demo, we're going to aggregate a number of "compliment" strings and then output them to the page. Where this would typically be done in some sort of For-Loop, functional programming abstracts looping through function-based expressions:
<!--- Include our functional programming methods. --->
<cfinclude template="./functional_udfs.cfm" />
<!---
Start a CFScript block. Closures can only be used inside
of CFScript due to the syntax required to define them.
--->
<cfscript>
// Define a function which returns a compliment dedicated
// to the given name.
function compliment( name ){
return( name & ", you are absolutely stunning." );
}
// I add an exclamation to the end of the string.
function exclamationize( value ){
return( reReplace( value, ".$", "!", "one" ) );
}
// Build up a colletion of friends with various properties,
// including NAME, which will be used to create compliments.
friends = [
{
name: "Tricia",
age: 37
},
{
name: "Joanna",
age: 25
},
{
name: "Kit",
age: 42
}
];
// Gather the compliments.
compliments = invokeWith(
arrayMap,
[
arrayPluck( friends, "name" ),
compose( compliment, exclamationize )
]
);
// Output the compliments.
writeDump( compliments );
</cfscript>
As you can see, not a single For-Loop or Array-Loop in sight. And yet, when we run the above code, we get the following output (array):
Tricia, you are absolutely stunning!
Joanna, you are absolutely stunning!
Kit, you are absolutely stunning!
There's a lot going on in this little bit of code. You're probably better off watching the video, but I'll point out some of the cooler features:
We have function composition. We are creating a function that pipes the result of one function - compliment() - into another function - exclamationize().
We have implicit mapping. The arrayPluck() function maps one set of objects onto another set of properties. In this case, we're mapping friends onto a collection of names. The names are being "plucked" out of the array.
We have indirect invocation. The arrayMap() function is be indirectly invoked with the invokeWith() function. The plucked names and the composite function are being used as the arrayMap's invocation arguments.
Not all of this code is necessary - I could have simply called arrayMap() directly, passing in the names and the composite function. I used indirect invocation just to show you the kind of stuff that can be possible with functional programming, which is facilitated by function expressions and closures.
I took a few hours last night to write up a whole number of Array-based functions and some utility functions. I won't bother explaining them, but feel free to poke through the code.
functional_udfs.cfm - Our Functional Programming Methods
<!---
Start a CFScript block. Closures can only be used inside
of CFScript due to the syntax required to define them.
--->
<cfscript>
// I return true if all elements in the given collection pass
// the given opreator.
function arrayAll( collection, oprerator ){
// Loop over all the elements in the collection.
for (var value in collection){
// Check to see if this value failes to pass the oeprator.
if (!operator( value )){
// We only need to FAIL ONE of the values. Since
// this one failes, let's break out of the loop.
// There's no need to continue checking.
return( false );
}
}
// If we made it this far, then all of the values passed the
// given oprerator.
return( true );
}
// I return true if any elements in the given collection pass
// the given operator.
function arrayAny( collection, operator ){
// Loop over all the elements in the collection.
for (var value in collection){
// Check to see if this value passes the oeprator.
if (operator( value )){
// We only need to match ONE of the values. Since
// this one matches, let's break out of the loop.
// There's no need to continue checking.
return( true );
}
}
// If we made it this far, then none of the values passed.
return( false );
}
// I create a new collection with the merger of each one of the
// given collections. Each collection is only flattened one level.
function arrayConcat(){
// Create a new result collection.
var newCollection = [];
// Merge each one of the colletions into the new collection.
for (var collection in arraySlice( arguments, 1 )){
// Merge the given collection with the new collection.
// This will only flatten ONE level.
arrayAppend( newCollection, collection, true );
}
// Return the new collection.
return( newCollection );
}
// I return a collection that consists of the given index
// extracted for each of the given sub-collections within the
// multi-dimentional array.
function arrayCrossSection( collection, index ){
// Create a cross-section collection to build.
var crossSection = [];
// Loop over each collection to inspect - we will take
// one value from each collection.
for (var i = 1 ; i <= arrayLen( collection ) ; i++){
// Extract the single value and push it on to the
// cross-section collection.
arrayAppend(
crossSection,
collection[ i ][ index ]
);
}
// Return the cross-section collection.
return( crossSection );
}
// I iterate over the given collection in reverse.
function arrayEachReverse( collection, operator ){
// Loop over the collection in reverse.
for (var i = arrayLen( collection ) ; i > 0 ; i--){
// Apply operator to the current value.
operator( collection[ i ] );
}
}
// I recursively flatten the given collections into one,
// single-dimension colletion.
function arrayFlatten(){
// Create a new colletion to build up.
var flattenedCollection = [];
// Loop over each argument to flatten and merge them
// individually into the result.
for (var argument in arraySlice( arguments, 1 )){
// Check to see if this argument is a collection - if so
// then each value within the colletion will have to be
// merged individually.
if (isArray( argument )){
// This argument is an array - we have to map each
// value onto a flattened value. This will cause a
// recursive exploration of this value.
arrayAppend(
flattenedCollection,
arrayMap(
argument,
function( value ){
// Return the flattened value.
return( arrayFlatten( value ) );
}
),
true
);
} else {
// Simply append the simple value.
arrayAppend( flattenedCollection, argument );
}
}
// Return the flattened collection.
return( flattenedCollection );
}
// I rturn the first N elements in the array; or, only the first
// item if headSize is not specified.
function arrayFirst( collection, headSize = 1 ){
// Check to see if the headSize is 1; if so, we'll just
// return the first element.
if (headSize == 1){
// Return th efirst element.
return( collection[ 1 ] );
} else {
// Return the leading values in the array.
return(
arraySlice( collection, 1, headSize )
);
}
}
// I fold the given operator over the collection, aggregating
// the return value. The opreator signature is:
//
// operator( memo, currentValue )
function arrayFold( collection, initialValue, operator ){
// Define the starting value of the memo as the provided
// inital value.
var memo = initialValue;
// Iterate over the array to apply the operator on each value.
for (var currentValue in collection){
// Apply the operator.
memo = operator( memo, currentValue );
}
// Return the aggregated value.
return( memo );
}
// I perform a fold-right across the collection [2..N], using the
// first value in the collection as the initial value.
function arrayFold1( collection, operator ){
// Pass off to fold-right.
return(
arrayFold(
arraySlice( collection, 2 ),
collection[ 1 ],
operator
)
);
}
// I folder the given operator over the collection in reverse,
// aggregating the return value. The operator signature is:
//
// operator( memo, currentValue )
function arrayFoldRight( collection, initialValue, operator ){
// Define the starting value of the memo as the provided
// inital value.
var memo = initialValue;
// Iterate over the collection in reverse.
for (var i = arrayLen( collection ) ; i > 0 ; i--){
// Apply operator to the current value.
memo = operator( memo, collection[ i ] );
}
// Return the aggregated value.
return( memo );
}
// I return the initial portion of the given collection,
// excluding the trailing portion of the given size.
function arrayInitial( collection, tailSize = 1 ){
// Return the leading portion of the collection.
return(
arraySlice(
collection,
1,
(arrayLen( collection ) - tailSize)
)
);
}
// I return the last N elements of the given collection; or, only
// the last item if N is not specified.
function arrayLast( collection, tailSize = 1 ){
// If the tailSize is 1, then only return the single item.
if (tailSize == 1){
return( collection[ arrayLen( collection ) ] );
} else {
// Return the element N elements.
return(
arraySlice(
collection,
(arrayLen( collection ) - tailSize + 1),
tailSize
)
);
}
}
// I create a new collection by mapping each value in the current
// collection using the given operator. NULL values are removed,
// arrays are merged.
function arrayMap( collection, operator ){
// Create a new collection to build.
var mappedCollection = [];
// Iterate over the collection to apply the operator to each
// value contained within.
arrayEach(
collection,
function( value ){
// Map the given value.
var mappedValue = operator( value );
// Check to see if the new value is null. If so, we'll
// exclude it from the new collection.
if (isNull( mappedValue )){
// Nothing to do here... this value will be
// excluded from the translated collection.
} else if (isArray( mappedValue )){
// Merge the array into the new collection. This
// will only flatten the array once.
arrayAppend(
mappedCollection,
mappedValue,
true
);
} else {
// Simply append the new value as-is.
arrayAppend( mappedCollection, mappedValue );
}
}
);
// Return the mapped colletion.
return( mappedCollection );
}
// I return true if none of the values in the given collection
// pass the given operator.
function arrayNone( collection, operator ){
// Simply negate the any() function. If at least one value
// matches, then we know that none() will fail.
return(
!arrayAny( collection, operator )
);
}
// I create a two-value array in which the first value holds
// collection items that passed the given operator (returned
// true), and the second value holds collection items that failed
// the given operator (returned value);
function arrayPartition( collection, operator ){
// Create the partitioned container. Each partition starts
// out as empty before we start colletion values.
var partition = [
[],
[]
];
// Loop over the collection to inspect with the operator.
for (var value in collection){
// Check to see if the value passes the operator -
// returns either true or false.
if (operator( value )){
// The value passed the operator. Add to the first
// partition.
arrayAppend(
partition[ 1 ],
value
);
} else {
// The value failed the operator. Add to the second
// partition.
arrayAppend(
partition[ 2 ],
value
);
}
}
// Return the bisected partition.
return( partition );
}
// I extract the given property from each item within the
// given collection.
function arrayPluck( collection, property ){
// Create our properties collection.
var propertyCollection = [];
// Loop over each item to access its property.
for (var item in collection){
// Extract the property and add to the plucked collection.
arrayAppend(
propertyCollection,
item[ property ]
);
}
// Return the plucked property collection.
return( propertyCollection );
}
// I return a new colletion that consists of the offset-delimited
// subset of elements from the given colletion.
function arrayRange( collection, startOffset, endOffset ){
// Normalize both offsets.
startOffset = normalizeArrayOffset( collection, startOffset );
endOffset = normalizeArrayOffset( collection, endOffset );
// Return the extracted sub-set of the collection.
return(
arraySlice(
collection,
startOffset,
(endOffset - startOffset + 1)
)
);
}
// I return a new collection excluding all values that failed
// to pass the given operator.
function arrayReject( collection, operator ){
// Pass off to filter(), but negate the operator first.
return(
arrayFilter(
collection,
negate( operator )
)
);
}
// I return a new array that is [2..N] of the given array.
function arrayRest( collection, headSize = 1 ){
// Return everything after the first index.
return(
arraySlice( collection, (headSize + 1) )
);
}
// I shuffle the given collection.
function arrayShuffle( collection ){
// Get the Java-based collections class that will actully
// execute the shuffling algorithm.
var util = createObject( "java", "java.util.Collections" );
// Shuffle the collection.
util.shuffle( collection );
// Return the shuffled collection.
return( collection );
}
// I goto the given offset and then delete the given number
// of elements and insert any additional arguments.
function arraySplice( collection, offset, deleteCount ){
// We'll need to build up a new collection.
var newCollection = [];
// Normalize the offset.
offset = normalizeArrayOffset( collection, offset );
// Check to see if the offset is beyond the first element.
if (offset > 1){
// Gather the pre-offset values.
arrayAppend(
newCollection,
arraySlice( collection, 1, (offset - 1) ),
true
);
}
// Check to see if we have arguments that we need to
// inject into the new collection.
if (arrayLen( arguments ) > 3){
// Inject all remaining elements (after the defined
// arguments list).
arrayAppend(
newCollection,
arraySlice( arguments, 4 ),
true
);
}
// Check to see if we need to add the tail of the original
// collection.
if ((offset + deleteCount) <= arrayLen( collection )){
// Append the tail of the original collection.
arrayAppend(
newCollection,
arraySlice( collection, (offset + deleteCount) ),
true
);
}
// Return the new collection.
return( newCollection );
}
// I return a two-index array that contains the leading and
// trailing portions of the array.
function arraySplit( collection, offset ){
// Create our two-part partition result. Default both
// portions to be empty arrays.
var partitions = [
[],
[]
];
// Normalize the offset.
offset = normalizeArrayOffset( collection, offset );
// Check to see if we have a leading portion to extract.
if (offset > 1){
// Pluck out the leading values UPTO offset (but not
// including the value at the offset - that will go in
// the trailing portion).
partitions[ 1 ] = arraySlice( collection, 1, (offset - 1) );
}
// Check to see if have a trailing portion to extract.
if (offset <= arrayLen( collection )){
// Pluck out the trailing values Including the value at
// the offcet.
partitions[ 2 ] = arraySlice( collection, offset );
}
// Return the partitions.
return( partitions );
}
// I return a new collection of leading values that DO NOT pass
// the given filter operator.
function arrayTakeUntil( collection, operator ){
// Pass off to the takeWhile() function, but negate the
// operator first.
return(
arrayTakeWhile(
collection,
negate( operator )
)
);
}
// I return a new collection of leading values that DO pass the
// given filter operator.
function arrayTakeWhile( collection, operator ){
// Create a collection of values that passes the operator.
var trueCollection = [];
// Loop over the values until we reach one that does NOT
// pass the operator.
for (var value in collection){
// Check to see if the value fails.
if (!operator( value )){
// The value did not pass the operator - break
// out of the loop, we are done collection.
break;
}
// This value passed the operator - append it to the
// true collection.
arrayAppend( trueCollection, value );
}
// Return whatever true values we have colleted so far.
return( trueCollection );
}
// I convert a collection into an index-keyed struct for use
// with the argumentCollection method invocation approach. In
// this struct, each index is the key, such that the first key
// is "1", the second key is "2", etc.
function arrayToArgumentCollection( collection ){
// Create our key-indexed collection.
var numericArguments = {};
// Loop over the array collection to translate the values
// into index-value pairs.
for (var i = 1 ; i <= arrayLen( collection ) ; i++){
numericArguments[ i ] = collection[ i ];
}
// Return the index-keyed colletion.
return( numericArguments );
}
// I return the given collection with the given value removed
// from the collection.
function arrayWithout( collection, value ){
// Pass off to filter.
return(
arrayFilter(
collection,
function( currentValue ){
// Only include the value if it does NOT match
// the value we are trying to remove.
return( currentValue != value );
}
)
);
}
// I am the default zipping collection which simple returns a
// zipped collection consisting of sub-collections composed of
// each cross-section.
function arrayZip(){
// Extract the collections from the arguments.
var collections = arraySlice( arguments, 1 );
// Pass this off to the arrayZipWith() function, using an
// operator that just returns the sub-set of arguments.
return(
callWith(
arrayZipWith,
collections,
function(){
return( arraySlice( arguments, 1 ) );
}
)
);
}
// I returned a zipped collection using the operator to generate
// the value based on the given collection.
function arrayZipWith(){
// Extract the collections and operator from the arguments.
// The collection will be everything [1..N-1].
var collections = arrayInitial( arguments );
var operator = arrayLast( arguments );
// Determine the zip length using the first collection.
// We'll assume that each collection is the same length
// for functional zipping.
var zipLength = arrayLen( collections[ 1 ] );
// Create a zipped-collection to populate. Each index of
// this collection will be the value returned from the
// opreator invocation.
var zippedCollection = [];
// Loop over the zip length
for (var i = 1 ; i <= zipLength ; i++){
// Invoke the operator with the cross-section of each
// of the sub-collections.
arrayAppend(
zippedCollection,
invokeWith(
operator,
arrayCrossSection( collections, i )
)
);
}
// Return the zipped collection.
return( zippedCollection );
}
// I invoke the given operator with the subsequent arguments.
// All arguments after the operator will be concatenated (ie.
// flattened one level) before they are passed to the oeprator.
function callWith(){
// Extract the operator and invoke arguments from the given
// set of arguments.
var operator = arrayFirst( arguments );
var values = arrayRest( arguments );
// Build up the invocation arguments.
var invokeCollection = [];
// Loop over each subsequent value (this may be a collection
// or it may be a simple value).
for (var value in values){
// Merge the collection onto the end.
arrayAppend(
invokeCollection,
value,
true
);
}
// Invoke the operator and return the result. When
// invoking the operator, we will use an index-keyed
// argument collection.
return(
invokeWith( operator, invokeCollection )
);
}
// I return a new function that applies the given functions
// in serial, pipping one result into the next.
function compose(){
// Get the methods in our pipeline.
var firstFunction = arrayFirst( arguments );
var functions = arrayRest( arguments );
// Create a new method that pipes the values through the
// composite chain of functions.
var pipeline = function(){
// Pass the incoming value through the pipeline.
var result = arrayFold(
functions,
firstFunction( argumentCollection = arguments ),
function( lastReturnValue, method ){
// Simply invoke the method and return it.
return(
method( lastReturnValue )
);
}
);
// Return the composite result.
return( result );
};
// Return the pipeline function.
return( pipeline );
}
// I invoke the operator with the given collection of arguments.
// This is somewhat like JavaScript's apply() method.
function invokeWith( operator, collection ){
// Invoke the operator after translating the index-based
// collection into a key-based collection.
return(
operator( argumentCollection = arrayToArgumentCollection( collection ) )
);
}
// I add caching to the given function. The argument comparisons
// are performed by the given hashing function (it generates the
// key at which the cached values are stored).
function memoize( operator, hashArguments ){
// Create a wrapper to our operator which attempts to cache
// the return values using the given arguments.
var memoizedOperator = function(){
// Get the key for the given set of arguments.
var hashKey = hashArguments( argumentCollection = arguments );
// Get the cached operator result, if possible.
var result = cacheGet( "memoized_#hashKey#" );
// Check to see if the result was found - if not, it may
// not have been calculated yet; or it may have been
// expelled from the cache.
if (isNull( result )){
// Get the result from the operator.
result = operator( argumentCollection = arguments );
// Cache the result for next time.
cachePut( "memoized_#hashKey#", result );
}
// Return the result which may have been freshly created
// of pulled from the cache.
return( result );
};
// Return the memoized function.
return( memoizedOperator );
}
// I normalize the offset to make it positive (if it was
// negative) and to make sure that it is in-bounds with the
// given collection. Raises OutOfBounds exception.
function normalizeArrayOffset( collection, offset ){
// Check to see if the offset is negative.
if (offset < 0){
// Convert to a positive equivalent offset.
offset = (arrayLen( collection ) + offset + 1);
}
// Make sure the offset is in-bounds.
if (
(offset < 1) ||
(offset > arrayLen( collection ))
){
throw( "OutOfBounds" );
}
// Return the normalized offset.
return( offset );
}
// I take an operator that returns a boolean value and decorate
// it so that it negates the return value.
function negate( operator ){
// Create a new function which negates the results.
var newOperator = function(){
// Flip the result.
return( !operator( argumentCollection = arguments ) );
};
// Return the decorated function.
return( newOperator );
}
</cfscript>
I think there's some really cool stuff here! Hopefully you're starting to see the value of closures and function expressions in ColdFusion 10!
Want to use code from this post? Check out the license.
Reader Comments
Very nice work with very clear explanations, as always, Ben! The functions you've created in functional_udfs.cfm will be very useful when we start building apps against CF10.
Looks very promising. How does CF10 compare to other java-like script languages such as Google's Dart and/or Scala?
@Brian,
Thanks! I had a lot of fun writing this up. I was especially happy with methods like memoize() and invokeWith().
@Marko,
I am not sure how to answer that. I don't know enough about those other languages. Like Scale (I think), ColdFusion also compiles down to Java byte code.
Very nice post, with great explanations! Your blog is my favourite resource for developing apps in cf... Many thanks!
@Ben,
Very nice library of higher order functions. I especially like memoize. A nice addition could be curry, which takes a function and some arguments, and partially applies those arguments to the function. For example,
would return a function that adds 7 to it's argument. It's very useful when mapping over values, though it can be difficult to implement when you have functions which take variable numbers of arguments (as in ColdFusion). Actually I believe this might be why Haskell doesn't support optional arguments...
It concerns me that some of these aren't built-in though. As someone who hails from a functional background, I'm confident in my ability to implement 'map', but it seems so basic to me that I'd expect it as a built-in function...
@Marko,
Scala is a language designed from the ground up for functional programming. It supports far more of the techniques common in functional programming than ColdFusion does, or realistically can (one example being pattern matching [1]). ColdFusion, on the other hand, is an imperative language designed from the ground up to support web development. ColdFusion 10 supports some functional programming techniques, but not as many as Scala (CF10 seems to have around as many as most imperative languages supporting functional techniques, e.g. JavaScript, Python, et al.). As Ben mentioned, both ColdFusion and Scala compile to java bytecode, but they have different (though, potentially overlapping) goals in mind, and the design of the languages and the programs written in them reflect that. Basically, they're hard to compare.
Dart, on the other hand is a programming language written by Google to replace JavaScript. It shares some similarities with CF and Scala, but from what I've seen the similarities are largely superficial. That said, I haven't ever written any Dart, so I might be wrong about that.
[1] http://www.scala-lang.org/node/120
@Thom, @Ben
I agree with Thom that it would be great / expected to see these functional functions implemented natively. I've just blogged about it here and linked back to your (excellent) post:
http://fusion.dominicwatson.co.uk/2012/03/my-first-bit-of-ruby%2C-nice-to-see-in-coldfusion.html