Skip to main content
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Sandy Clark
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Sandy Clark ( @sandraclarktw )

Code Kata: Flattening An Array In Lucee CFML

By on
Tags:

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.

Having Multiple Approaches is a Language Strength

One opinion that I often hear people in other programming communities voice is that there should be one idiomatic way to do things. In ColdFusion, this is the farthest thing from the truth. With CFML, there are multiple ways to do most things; and, I find that to be a huge strength of the platform. You get to pick the approach that feels right for the context - you don't have to shoe-horn all manner of variety into some inflexible set of constructs.

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

Reader Comments

15 Comments

Interesting assessment.

You could improve your reduce version by getting rid of a bunch of boilerplate which one wouldn't usually introduce in these simple situations. Here's a running tested example:

https://trycf.com/gist/2d6349d9a9cccc1c0312d0b59e488908/acf2021?setupCodeGistId=816ce84fd991c2682df612dbaf1cad11&theme=monokai

public array function improvedFlatten() {
    return arrayReduce(
        arguments,
        (flattened, value) => flattened.append(value, true),
        []
    )
}

The conceit is that if yer reducer function is a single expression: no need for the braces or the return. Or, TBH, pointless intermediary variables (ie: results serves no purpose even in your example).

I'm also using a better approach to reducing... you don't need the key in this case, so there's no point doing a struct iteration over what you really need to be an array (by definition).

From a clean code perspective, I'm not crazy about how you've not used a parameter in a function that definitely takes parameters. Looks to me like you wanted to make a comment about how the arguments scope can be used as both a struct or array (good), but this is not a good example of how to go about that, as you are using a struct operation when you actually want an array operation.

Worthy of note that CF - but not Lucee, and I realise this is lucee-specific code - does actually support variadic functions properly, eg:

https://trycf.com/gist/55229b824d6a5f2c50a2828422aef7f1/acf2021?setupCodeGistId=816ce84fd991c2682df612dbaf1cad11&theme=monokai

function squishArguments(x, ...y) {
    return arguments
}

squishArguments("a", "b", "c", "d") // [x="a", y=["b", "c", "d"]]

It's probably also noting that using the each collection iteration functions to build a data structure is not an idiomatic use of said functions. One ought to use map / reduce / filter etc as appropriate.

Good stuff exposing all this to the CFML community though. I bet a lot of your readers wouldn't have considered the various options available to them.

Deeply disappointed you did not use a tag island for the <cfloop> ;-)

15,377 Comments

@Adam,

This might sound silly, but I've been using member methods for so long now, I literally forgot that the built-in function, arrayReduce(), even existed ๐Ÿ˜‚ Speaking to your point about doing array-reduction instead of struct-reduction.

To that point, I also forgot that ColdFusion introduced the Rest and Spread operators. Though, since I work mostly in Lucee CFML, that memory gap is, at least, a bit more understandable.

And, I agree with your sentiment that I probably should have just used an argument instead of trying to use a variadic method. In fact, when I did the follow-up post on recursive flattening, I used a values argument (array), which greatly simplified the approach, allowing me to use a simple:

for ( var value in values ) { ... }

As far as superfluous syntax -- I just love me some parenthesis and braces. It goes along with my general love of white-space. 100% subjective.

I promise to have at least 5 tag-islands in my next post!

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

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.
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