Skip to main content
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Stéphane Vantroyen
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Stéphane Vantroyen

Exploring Variadic Function Mechanics In ColdFusion

By
Published in Comments (8)

A variadic function is a function with an open-ended number of arguments. If you're familiar with the JavaScript world, functions like Array() and console.log() are variadic and work fine when passed no arguments, one argument, two arguments, etc. In the ColdFusion world, we can also create variadic functions; but, getting them to work in both Lucee CFML and Adobe ColdFusion takes a little trial-and-error.

We've Always Had Variadic Functions In ColdFusion

Variadic function mechanics in ColdFusion are nothing new. Ever since we've had user defined functions (UDF), we've been able to create open-ended arguments. This is because the arguments scope in ColdFusion is a special scope implementation that can be consumed both as a struct (with named arguments) and as an array (with ordered arguments).

As such, we've always been able to do something like this, where we loop over the length of the arguments object and collect the index-based elements:

<cfscript>

	writeDump( testArgs() );
	writeDump( testArgs( "one" ) );
	writeDump( testArgs( "one", "two" ) );
	writeDump( testArgs( "one", "two", "three" ) );
	writeDump( testArgs( "one", "two", "three", "four" ) );

	// Exploring variadic method mechanics.
	public array function testArgs( /* .... */ ) {

		var args = [];

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

			arrayAppend( args, arguments[ i ] );

		}

		return args;

	}

	// Lucee: Works!
	// Adobe: Works!

</cfscript>

As you can see, we're treating the arguments collection as an array; and we're creating a secondary array, args, that collects all the elements, 1...N, and returns them. When we run this CFML code in either Adobe ColdFusion or Lucee CFML we get the following:

Five different arrays being dumped-out to the screen. The first array is empty; and, each successive array contains one additional element.

This approach has always worked; but, the CFML language has come a long way since UDFs were first introduced. So, the real question is, can we come up with a more elegant approach using modernized syntactic mechanics.

Failure: The Rest Operator

In many programming languages, the rest operator is exactly the solution we're looking for. The rest operator acts as a "sink" that will consume any number of arguments that haven't already been consumed by a previously defined parameter:

<cfscript>

	writeDump( testArgs() );
	writeDump( testArgs( "one" ) );
	writeDump( testArgs( "one", "two" ) );
	writeDump( testArgs( "one", "two", "three" ) );
	writeDump( testArgs( "one", "two", "three", "four" ) );

	// Exploring variadic method mechanics.
	public array function testArgs( ...inputs ) {

		return inputs;

	}

	// Lucee: Fails due to lack of rest operator support.
	// Adobe: Fails when only a single argument is passed-in.

</cfscript>

In this approach, the ...inputs parameter is telling ColdFusion to slurp-up all the arguments and store them into the inputs variable as an array. Unfortunately, Lucee CFML hasn't implemented the rest operator; and, Adobe ColdFusion's implementation of the rest operator is fundamentally flawed.

Failure: The Spread Operator

The rest operator has a counterpart known as the spread operator. The spread operator takes a collection of values and merges them (ie, spreads them) into another expression. If you're familiar with the world of ReactJS, the spread operator is heavily used in an anti-pattern known as "prop drilling".

Even though we can't use the rest operator to collection arguments, we might be able to use the spread operator to collect arguments internally to the function body:

<cfscript>

	writeDump( testArgs() );
	writeDump( testArgs( "one" ) );
	writeDump( testArgs( "one", "two" ) );
	writeDump( testArgs( "one", "two", "three" ) );
	writeDump( testArgs( "one", "two", "three", "four" ) );

	// Exploring variadic method mechanics.
	public array function testArgs( /* .... */ ) {

		return [ ...arguments ];

	}

	// Lucee: Fails due to lack of spread operator support.
	// Adobe: Fails due to strange Java class leakage.

</cfscript>

Here, we're defining an array literal and then attempting to spread the arguments into the array literal definition. Unfortunately, Lucee CFML hasn't implemented the spread operator; and Adobe ColdFusion's implementation doesn't work with the arguments object.

When we run this code in Adobe ColdFusion 2023, we get the following output:

Several arrays being output to the screen. Each array contains

As you can see, the spread operator executes; but, the underlying Java class that represents the function parameters leaks up into the ColdFusion space.

Failure: ArraySlice()

We already know that we can treat the arguments collection as both a struct and as an array (see first example). So, perhaps we can use more modern array techniques to collection the arguments. Here's an arraySlice() approach:

<cfscript>

	writeDump( testArgs() );
	writeDump( testArgs( "one" ) );
	writeDump( testArgs( "one", "two" ) );
	writeDump( testArgs( "one", "two", "three" ) );
	writeDump( testArgs( "one", "two", "three", "four" ) );

	// Exploring variadic method mechanics.
	public array function testArgs( /* .... */ ) {

		return arrayIsDefined( arguments, 1 )
			? arraySlice( arguments, 1 )
			: []
		;

	}

	// Lucee: Fails because cannot cast ArgumentImpl to an ArrayList.
	// Adobe: Works!

</cfscript>

With arraySlice(), we have to be careful not to go past the bounds of the array; which is why we need to check to see if the first element is defined. This approach works in Adobe ColdFusion; but, fails in Lucee CFML with the following error:

class lucee.runtime.type.scope.ArgumentImpl cannot be cast to class java.util.List (lucee.runtime.type.scope.ArgumentImpl is in unnamed module of loader org.apache.felix.framework.BundleWiringImpl$BundleClassLoader @cc239ba; java.util.List is in module java.base of loader 'bootstrap')

As an aside, I often create custom array functions like arraySliceSafe() to make it safe to slice on an empty array.

Failure: ArrayAppend([all])

If we can't slice the arguments object in Lucee CFML, perhaps we can "append all" of it to another array. In the following code, we'll pass the optional boolean flag to arrayAppend(), telling ColdFusion to spread the incoming array to the end of the target array:

<cfscript>

	writeDump( testArgs() );
	writeDump( testArgs( "one" ) );
	writeDump( testArgs( "one", "two" ) );
	writeDump( testArgs( "one", "two", "three" ) );
	writeDump( testArgs( "one", "two", "three", "four" ) );

	// Exploring variadic method mechanics.
	public array function testArgs( /* .... */ ) {

		return [].append( arguments, true );

	}

	// Lucee: Works!
	// Adobe: Fails because arguments is appended as a single Struct.

</cfscript>

Here's we're defining an array literal and then attempting to append the contents of the arguments collection into it. This works in Lucee CFML; but, unfortunately Adobe ColdFusion is treating the arguments collection as a struct. As such, we end up with an array that as a single element:

Several array dumps showing that each array contains only a single element which is a struct version of the arguments collection.

Success: ArrayMap()

And finally, we come to the modern approach that works in both Lucee CFML and Adobe ColdFusion: arrayMap(). The arrayMap() built-in function allows us to map the index-based arguments onto a true ColdFusion array:

<cfscript>

	writeDump( testArgs() );
	writeDump( testArgs( "one" ) );
	writeDump( testArgs( "one", "two" ) );
	writeDump( testArgs( "one", "two", "three" ) );
	writeDump( testArgs( "one", "two", "three", "four" ) );

	// Exploring variadic method mechanics.
	public array function testArgs(	 /* .... */ ) {

		return arrayMap( arguments, ( element ) => element );

	}

	// Lucee: Works!
	// Adobe: Works!

</cfscript>

For the sake of brevity, I'm using the fat-arrow, short-hand notation, allowing us to perform this transformation on a single line. And when we run this in both Adobe ColdFusion and Lucee CFML, we get the following output:

Side-by-side browsers showing Lucee CFML and Adobe ColdFusion outputs; each of which shows 5 arrays, with each array containing a subsequent argument.

In a perfect world, we'd have the rest operator in both Adobe ColdFusion and Lucee CFML (and it would be implemented correctly). But, until that happens, the arrayMap() approach feels elegant enough.

Epilogue: Using Boxlang

For funzies, I wanted to see which of these would work in Boxlang. Unfortunately, I couldn't get Boxlang to run locally in CommandBox (I might not have a compatible version installed). But, they do have an online playground I was able to use. Here are the results:

  • Historic approach: Works!

  • Rest operator: Fails with error, "Invalid syntax, '.' was unexpected while parsing an identifier in an expression".

  • Spread operator: Fails with error, "Error compiling, '.' was unexpected while parsing an identifier in an expression in an array literal in an expression".

  • arraySlice(): Works!

  • arrayAppend([all]): Works!

  • arrayMap(): Works!

I'm surprised the rest operator failed since Boxlang is documented to support it. But at least it seems that the arrayMap() approach works in all of the major CFML runtimes!

For Brad or Luis, here's the error that CommandBox was giving me when trying to start the Boxlang server:

[INFO ] Runwar: Configuring Servlet
[INFO ] Runwar:   Found WEB-INF: '/Users/bennadel/.CommandBox/server/D8EF596789E9B6CA37D860F23E0B5B8D-boxlang/boxlang-1.0.0-snapshot/WEB-INF'
[ERROR] Runwar:     Error reading web.xml
java.lang.IllegalArgumentException: UT010009: Servlet BoxLangServlet of type class ortus.boxlang.servlet.BoxLangServlet does not implement javax.servlet.Servlet
	at io.undertow.servlet.api.ServletInfo.<init>(ServletInfo.java:76) ~[runwar-5.0.8.jar:5.0.8]
	at runwar.undertow.WebXMLParser.lambda$parseWebXml$3(WebXMLParser.java:112) ~[runwar-5.0.8.jar:5.0.8]
	at org.joox.Impl.each(Impl.java:407) ~[runwar-5.0.8.jar:5.0.8]
	at org.joox.Impl.each(Impl.java:79) ~[runwar-5.0.8.jar:5.0.8]
	at runwar.undertow.WebXMLParser.parseWebXml(WebXMLParser.java:99) [runwar-5.0.8.jar:5.0.8]
	at runwar.RunwarConfigurer.configureServerWar(RunwarConfigurer.java:128) [runwar-5.0.8.jar:5.0.8]
	at runwar.RunwarConfigurer.configureServerResourceHandler(RunwarConfigurer.java:87) [runwar-5.0.8.jar:5.0.8]
	at runwar.Server.startServer(Server.java:332) [runwar-5.0.8.jar:5.0.8]
	at runwar.Start.main(Start.java:44) [runwar-5.0.8.jar:5.0.8]
[ERROR] java.lang.RuntimeException: java.lang.IllegalArgumentException: UT010009: Servlet BoxLangServlet of type class ortus.boxlang.servlet.BoxLangServlet does not implement javax.servlet.Servlet
[ERROR] 	at runwar.undertow.WebXMLParser.parseWebXml(WebXMLParser.java:300)
[ERROR] 	at runwar.RunwarConfigurer.configureServerWar(RunwarConfigurer.java:128)
[ERROR] 	at runwar.RunwarConfigurer.configureServerResourceHandler(RunwarConfigurer.java:87)
[ERROR] 	at runwar.Server.startServer(Server.java:332)
[ERROR] 	at runwar.Start.main(Start.java:44)
[ERROR] Caused by: java.lang.IllegalArgumentException: UT010009: Servlet BoxLangServlet of type class ortus.boxlang.servlet.BoxLangServlet does not implement javax.servlet.Servlet
[ERROR] 	at io.undertow.servlet.api.ServletInfo.<init>(ServletInfo.java:76)
[ERROR] 	at runwar.undertow.WebXMLParser.lambda$parseWebXml$3(WebXMLParser.java:112)
[ERROR] 	at org.joox.Impl.each(Impl.java:407)
[ERROR] 	at org.joox.Impl.each(Impl.java:79)
[ERROR] 	at runwar.undertow.WebXMLParser.parseWebXml(WebXMLParser.java:99)
[ERROR] 	... 4 more

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

Reader Comments

15,978 Comments

@Zac, ahh, much appreciated good sir! Thank you for getting that in there. Also, good to see some new core functions being added. I often have utility functions that have to build in that workflow, things like arrayIndexBy() and arrayGroupBy(). I love when stuff gets taken-in by the runtime itself. Makes my life easier!

15,978 Comments

@Chris,

Yeah, it's so wild that they've created another CFML engine! Seems like such an enormous undertaking. Over on LinkedIn, Luis was saying that my error above (when trying to run a boxlang server) was because I hadn't installed the boxlang module for CommandBox; though, I tried to install it and it didn't seem to help. I'm probably just doing something wrong. I really don't know CommandBox very well.

15,978 Comments

@Zac,

I don't think I've seen that index stuff before. The page you linked to, the example throws some sort of type casting error; but, I wasn't able to see what was actually going wrong. That said, I think I get the gist of what's happening - you can define one of the columns to be accessible by its value. That's pretty cool!

259 Comments

@Ben Nadel,

HUGE undertaking and considering how much they've already accomplished (and support) I'm (very) impressed that they even found the time.

Ray Camden is a solid get as their Boxlang evangelist as well. Go Ray!

I love that we do, but not sure why we needed another engine. I think it must have a lot to do with provisioning license policies surrounding cloud based infrastructure. I understand ACF is pretty cringe in this area (but getting better?). This seems to be the area of greatest opportunity to compete and interrupt. I'm fairly uneducated in this regard though.

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

Post a Comment

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