Why the image of a water pipe with flowing water? It will all make sense soon, my dear reader.
The Premise
Given a bunch of arrays kept within a JavaScript hash table (plain object), we want to extract the arrays and combine them. In other words, we’re given a collection of arrays of elements and we want a single array of elements.
This example was inspired by some code I found in the codebase where I work. The use case was different, but the overall idea (extracting elements from within arrays that are within an object) is the same. To make things slightly more complex, the arrays of the input object could possibly contain null
elements because the elements were being provided by a service that could sometimes return null
.
Example Input/Output
The output has the null
s removed. We can pretty much assume we only want to see user objects in the output array; no other kinds of elements.
The Original Solution
The following code snippet is a slightly modified version of someone else’s work. I’ve changed the variable names and comments, but the core logic/algorithm is the same.
- The combination of
_.each
andArray.prototype.concat
creates one big array from all the arrays within the input object calledkeyedArrays
. - The combo of
filter
andBoolean
rids the big array of falsey values to ensure nonull
elements end up in the output.
Let’s Refactor!
Refactor 1 - Using Lodash’s Chain
Sadly, we need to create our own concatArray
because Lodash doesn’t have such a utility method (I swear it used to exist in an earlier version …maybe).
Thankfully, we can actually use Lodash’s reduce
on objects (not just arrays). I see the replacement of each
with reduce
as a win because the end result is more expressive. each
is vague whereas reduce
makes it more clear that we intend to go from a collection of things (in this case, a collection of arrays) to just a single thing (in this case, just a single array).
Refactor 2 - Using Lodash’s Flow
Now we use function composition via flow
, which uses left-to-right direction. Standard function composition via compose
would read from right-to-left, but I prefer LTR for a more familiar aesthetic. My friends who are more advanced in functional programming assure me I’ll get used to the RTL direction if I give it a shot, but for now, I protest (i.e., I’m lazy).
With flow
, we can read combineKeyedArrays
as a series of 3 steps. First, we extract values from an object via values
, then we flatten the resulting array via flatten
, then we reject any falsey elements from the array via compact
.
Notes:
values
obviates the need for the combo ofreduce
+concat
flatten
is shallow by defaultcompact
obviates the need for the combo offilter
+Boolean
OMG WHERE DID THE INPUT/PARAMETER GO?!
–You (probably)
We can stop referring to the input as keyedArrays
. Our function combineKeyedArrays
has now been written in a pointfree (aka point-free aka tacit) style. In other words, we no longer need to name - and refer back to - any parameter variable.
Think of it like the verbs “hit” and “type” in the English language. The word “hit” is a bit vague, so you probably should include more context or references for clarity. Are you hitting a person in a fight? Are you hitting some books to study? Are you hitting the bed to sleep? Are you hitting a keyboard button to type?
The word “type” is more specific. You already can infer you’ll be dealing with a keyboard. You don’t need to mention the keyboard at all when you use the verb “type” instead of the verb “hit”.
“I’m typing UNIX commands” is more concise and direct than “I’m hitting buttons on the keyboard to issue UNIX commands”. Both are valid, but the former is easier to understand even though it’s less comprehensive.
Refactor 3 - Using Ramda
Now, let’s translate from Lodash to Ramda, a utility library that is much more aligned with the functional programming style. I’ve covered how to get started with Ramda in an earlier blog post that one friend labeled as an “excellent summary”. I must be pretty awesome :D.
Notes:
pipe
is Ramda’s_.flow
. I appreciate the name “pipe” over “flow” because “pipe” reminds me of Bash’s|
operator.unnest
is Ramda’s shallow array-flattening method.- Ramda lacks a
compact
:( R.filter(Boolean)
is leveraging currying / using partial application to yield the same effect as_.compact
.
Let’s Review
We’ve gained so much:
- Expressiveness! Remember that
each
is vague; the refactored versions usingflow
andpipe
are far more direct and straightforward (assuming you’re familiar with the library methods). Also, the combo of_.chain
and_.value
adds unnecessary boilerplate cruft compared to the simplicity offlow
orpipe
. - Brevity! Shorter code isn’t always better code, but if expressiveness and legibility remain high as code length decreases, that’s generally what scientists refer to as a victory.
- Robustness! We’re using well-tested library methods. There are fewer possible typos after refactoring to simpler code.
- Fun! Wasn’t that so much fun?! Hell yeah it was!! *level-up*
By the way, possible documentation for our refactored, pointfree combineKeyedArrays
involves a type signature as a comment, but admittedly, I’m still learning how to do proper, FP-style type signatures. Also, keep in mind that the names of your functions should help tell others what it does, and the fact that it’s composed of 3 easy-to-read methods is quite helpful as well.
Why don’t we say something more specific such as // {k: [user]} --> [user]
? Because combineKeyedArrays
clearly works with any type of element inside the arrays. It could even be considered as a utility function and added to an internal library of helpers. Whoooaaaaaa…
And because I appreciate you as a cool person, here’s a Gist that has all the code in one spot for your future reference.