Flattening An Array In Lucee CFML

Cyberdime
Published: January 7, 2023

Yesterday, at InVision, I was writing an algorithm in which I needed to build several one-dimensional arrays. And, in some cases, I was using all simple values; but, in other cases, I was using a mixture of simple values and other arrays. To keep my calling code clean, I abstracted the logic into a flattenArray() method that would take N-arguments and then smoosh all of those arguments down into a single array. The method I created worked fine, but it just didn’t look “right”. I wasn’t vibing it. As such, I wanted to step back and try creating a flatten method with a variety of different syntaxes to see which strikes the right balance between simplicity, elegance, and readability (which is all highly subjective).

In my case, I only needed the method to flatten one level deep – I wasn’t going to be using any deeply-nested arrays. As such, at least my logic didn’t require any recursion; so, that’s already a win from the get-go. Flattening an array in this manner turns:

[a, [b, c], d]

… into:

[a, b, c, d]

Note that the [b,c] array was “unwrapped” and merged into the final result.

Here are four different approaches that I can think of to flatten an array in ColdFusion (without recursion). I am using .reduce(), .each(), and two different types of loops:

<cfscript>
	a = [ "hello", "world" ];
	b = "simple";
	c = [ "cool", "beans" ];
	// NOTE: All of these methods only flatten ONE LEVEL down.
	dump( arrayToList( flatten( a, b, c ) ) );
	dump( arrayToList( flatten2( a, b, c ) ) );
	dump( arrayToList( flatten3( a, b, c ) ) );
	dump( arrayToList( flatten4( a, b, c ) ) );
	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //
	// APPROACH ONE: Using the .reduce() method.
	public array function flatten() {
		var results = arguments.reduce(
			( reduction, key, value ) => {
				return( reduction.append( value, isArray( value ) ) );
			},
			[]
		);
		return( results );
	}

	// APPROACH TWO: Using CFLoop for array values.
	public array function flatten2() {
		var results = [];
		loop
			item = "local.value"
			array = arguments
			{
			results.append( value, isArray( value ) );
		}
		return( results );
	}

	// APPROACH THREE: Using a for-in loop.
	public array function flatten3() {
		var results = [];
		for ( var key in arguments ) {
			results.append( arguments[ key ], isArray( arguments[ key ] ) );
		}
		return( results );
	}

	// APPROACH FOUR: Using an each iterator.
	public array function flatten4() {
		var results = [];
		arguments.each(
			( key, value ) => {
				results.append( value, isArray( value ) );
			}
		);
		return( results );
	}
</cfscript>

When we run this code, all four flatten methods yield the same output:

Four different flatten outcomes all showing: hello, world, simple, cool, beans

So, these flatten methods all “work”, but which one is the “best”?

As always, one of my first instincts is to use the .reduce() method. There is something so alluring about .reduce() – it has an air of sophistication and an elegance underscored by classical computer science. I actually feel smarter when I write a .reduce() method.

That said, just about every time I’m done writing a .reduce() method, I step back and just feel so meh about the whole thing. .reduce() always feels way too wordy with lots of values and syntactic noise. As such, 9-in-10 times, I scrap the .reduce() approach and use a simplified loop.

At work, I ended up going with approach two: using the CFLoop tag to iterate over the arguments collection. One thing that I love about the CFLoop tag is that it can expose a number of optional attributes that can surface different aspects of the iteration. Meaning, when iterating over a Struct, I can use both the key and value attributes; or, just one of them. Similarly, with an Array, I can use both the item and index attributes; or, just one of them. In other words, the CFLoop tag allows me to define only the parts of the loop that I actually need to consume. In my case, I’m exposing the item aspect of Array iteration without the index since I don’t actually need the index.

ASIDE: The arguments scope is neither an Array nor a Struct – it’s a specialized scope that has both Array and Struct behaviors, which makes it some kind of wonderful. Calling isArray(arguments) and isStruct(arguments) both yield true.

The CFLoop tag approach also feels like it does the most work with the least amount of syntax.

If, instead of creating a variadic method (a method that receives a dynamic number of arguments), I created a method that received a single argument which was an array, then I would probably go with the for-in style loop:

<cfscript>
	public array function flatten( required array values ) {
		var results = [];
		for ( var value in values ) {
			results.append( value, isArray( value ) );
		}
		return( results );
	}
</cfscript>

In my case, since I am using a variadic method, the for-in approach uses Struct iteration (of the arguments scope), not Array iteration. Which means I have to perform a key-based look-up of the iteration value.

I know this stuff is highly subjective. When I look at the different techniques, I just have to listen to my gut and go with the method that feels like it strikes the right balance of qualities.

Check out the license.

Source: www.bennadel.com