Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at RIA Unleashed (Nov. 2009) with: Kevin Hoyt
Ben Nadel at RIA Unleashed (Nov. 2009) with: Kevin Hoyt@parkerkrhoyt )

Upgrading A Built-In Function To A First-Class Citizen In Lucee 5.3.2.77

By Ben Nadel on
Tags: ColdFusion

For as long as I can remember, ColdFusion has supported the idea of User Defined Functions (UDFs) as First-Class Citizens. Meaning, User Defined Functions can be referenced and passed-around just like any other object in the ColdFusion landscape. The same, however, has never been true of Built-In Functions (BFIs). Attempting to pass-around a BIF like ucase or trim results in a variable doesn't exist error. That said, with member-methods like .map() and .each(), I've often wished that I could treat a BIF like a UDF. As such, I wanted to see how difficult it would be to create a Closure that would, for all intents and purposes, upgrade a BIF to be a First-Class Citizen in Lucee 5.3.2.77.

To set the context for this exploration, one thing that I've wanted to do is be able to pass a Built-In Function, like trim, to a .map() operation:

<cfscript>

	values = values.map( trim );

</cfscript>

Or, be able to choose between two different Built-In Functions based on some additional piece of information:

<cfscript>

	operator = ( doUppercase )
		? ucase
		: lcase
	;

	echo( operator( "I said Good-Day, sir." ) );

</cfscript>

But, these use-cases will throw an error in both Adobe ColdFusion as well as Lucee CFML. That said, CFML - being a sweet-ass dynamic language - allows us to "construct and evaluate" code at runtime. Specifically, we can use the evaluate() function to execute a string of CFML code that we build within our CFML code. This, combined with the fact that Closures can be returned by Higher-Order Functions, means that we should be able to construct a Closure that proxies an underlying Built-In Function.

ASIDE: Lucee CFML also has a render() function, in addition to the evaluate() function. I have not yet tried to use the render() function; and, I am not entirely sure how it differs from evaluate(). It has a dialect options ("cfml" vs "lucee"). And, I believe it can handle Tag-based code in addition to Script-based code.

One complexity of using this approach is that I want to be able to use Built-In Functions in places for which they are not necessarily the most natural fit. Going back to my .map() example above, the .map() callback is not a unary operator. When the .map() callback is invoked, it is passed several arguments:

values.map( closure( value [, index [, collection ] ] ) )

If we wanted to use a Built-In Function, like trim() - which takes one argument - we'd have to translate the 3-arguments passed to the .map() callback down to the 1-argument passed to .trim() BIF. If we were operating under the constrains of a Functional language, we might have to proxy our trim() function through something like a unary() function which would do this translation for us:

values.map( unary( trim ) )

But, we're using ColdFusion, which is, once again, hella dynamic. One of the many cool features of ColdFusion is the ability to reflect and introspect the state of the runtime. In this case, we can use a Lucee-specific function, getFunctionData(name), to gather meta-data about a Built-In Function. Specifically, we can see the arity of a given Function (the number of arguments it accepts). And, we can then use this reflected arity to pare down, or translate, the number of invocation arguments.

Let's try this out. In the following code, I'm going to create a User Defined Function (UDF) called upgradeBIF(), which takes the name of a Built-In Function and returns a Closure that proxies the invocation of said BIF:

<cfscript>

	values = [ "oLLeH", "Ym", "dOOg", "DnEiRf" ];

	// NOTE: The .map() operations in the following calls will pass-in several arguments
	// to the provided operator ("reverse", "ucase", "lcase"); however, these Functions
	// can only accept one argument. As such, our proxy function will pare down the
	// arguments based on built-in function's native arity.

	values = values.map( upgradeBIF( "reverse" ) );

	// Randomly choose an operator for our next .map() call.
	operator = randRange( 0, 1 )
		? upgradeBIF( "ucase" )
		: upgradeBIF( "lcase" )
	;

	dump( values.map( operator ) );

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

	/**
	* I attempt to upgrade the given built-in function (BIF) such that it can be passed-
	* around like a first-class Function reference.
	* 
	* @name I am the name of the BIF to upgrade.
	*/
	public Function function upgradeBIF( required string name )
			cachedWithin = "request"
		{

		var bifMetadata = getFunctionData( name );
		// The getFunctionData() function returns meta-data about the built-in function
		// with the given name. This includes the array of arguments that it expects,
		// which can use to determine the arity of the function (ie, how many arguments
		// it expects).
		var bifArity = bifMetadata.arguments.len();

		return(
			() => {

				// If there are no arguments - the Built-In Function (BIF) doesn't expect
				// arguments - pass-through an empty set of arguments.
				if ( ! bifArity || ! arguments.len() ) {

					var argumentList = "";

				// If the arguments passed to the proxy function are provided as an
				// ordered list of values, then we have to pare down the collection to
				// just those expected by the Built-In Function's (BIF) native arity (or
				// ColdFuison will throw an error).
				// --
				// NOTE: We are loosely determining argument-type by seeing if an index-
				// oriented argument key exists.
				} else if ( arguments.keyExists( "1" ) ) {

					var argumentList = arguments
						.keyArray()
						.slice( 1, min( bifArity, arguments.len() ) )
						.map( ( index ) => "arguments[ #index# ]" )
						.toList( "," )
					;

				// If the arguments passed to the proxy function are provided as a set of
				// key-value pairs, then we need to pass them through to the Built-In
				// Function (BIF) in the same manner.
				// --
				// CAUTION: While some BIFs support argumentCollection (those that are
				// implemented in CFML), the vast majority of BIFs do not support this
				// type of invocation. As such, we have to explicitly create a listed of
				// named arguments.
				} else {

					var argumentList = arguments
						.keyArray()
						.map( ( key ) => "#key# = arguments.#key#" )
						.toList( "," )
					;

				}

				return( evaluate( "#name#( #argumentList# )" ) );

			}
		);

	}

</cfscript>

As you can see, the upgradeBIF() function takes the name of a Built-In Function and returns a Closure that proxies a call to evaluate(). The bulk of the logic in this demo is the translation of Named and Ordered arguments into a set of parameters that can be used to invoke the Built-In Function.

Now, when we run the above Lucee CFML code, we get the following output:

Passing built-in functions to the .map() method in Lucee 5.3.2.77.

As you can see, we were able to pass the Built-In Functions, reverse() and lcase(), to the .map() member method of the Array.

This was just a fun experiment and an exploration of the dynamic nature of Lucee CFML. I doubt I would actually do this in a production app. In all likelihood, I would just inline a closure that explicitly invokes the underlying Built-In Function. But, it's cool to see that this is possible in Lucee CFML 5.3.2.77. I really love how dynamic ColdFusion is!


Reader Comments

Holy guacamole! My mind is officially blown...

Might have to read this a few more times, until I understand it!

I never knew about:

getFunctionData()

Another little nugget to put in my back pocket.

And is:

arguments.keyExists( "1" )

The same as:

StructKeyExists(arguments,"1")
Reply to this Comment

@Charles,

Yeah, I only just learned about getFunctionData() also -- it's what gave me the idea for this exploration ;)

And, yes - structKeyExists() is the same as struct.keyExists(). Though, only the latter works on official "Struct Implementations", while I assume the former will work a wider array of Java Hashes ... though, I'm just guessing. I could be wrong about that.

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.