Code Kata: Flattening An Array In Lucee CFML
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:
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. CallingisArray(arguments)
andisStruct(arguments)
both yieldtrue
.
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
As a quick follow-up post, I wanted to take a look at iterating over the
arguments
scope as either an Array or a Struct viaCFLoop
:www.bennadel.com/blog/4383-iterating-over-function-arguments-using-cfloop-in-lucee-cfml.htm
The
CFLoop
tag and thearguments
scope are both very dynamic, and work well together.As another fast-follow, I wanted to look at recursively flattening a deeply nested array:
www.bennadel.com/blog/4384-code-kata-recursively-flattening-a-deep-array-in-lucee-cfml.htm
In the recursive version, I'm passing in a
values
argument instead of using a variadic, which is why I end up using thefor-in
approach to iterating over the given Array value. This keeps the syntax about as simple as it can get.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
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
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>
;-)@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: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! ❤️
Post a Comment →