I've seen the problem called round-robin, but maybe interleave
is a better name. Sure, map
, reduce
, and filter
are functional procedures, but not all functional programs need rely on them. When these are the only functions we know how to use, the resulting program is sometimes awkward because there is often a better fit.
map
produces a one-to-one result. If we have 4 subarrays, our result will have 4 elements. interleave
should produce a result equal to the length of the combined subarrays, so map
could only possibly get us part way there. Additional steps would be required to get the final result.
reduce
iterates through the input elements one-at-a-time to produce a final result. In the first reduce, we will be given the first subarray, but there's no straightforward way to process the entire subarray before moving onto the next one. We can force our program to use reduce
, but in doing so, it makes us think about our collation procedure as a recuding procedure instead of what it actually is.
The reality is you're not limited to the use of these primitive functional procedures. You can write interleave
in a way that directly encodes its intention. I think a interleave
has a beautiful recursive definition. I think the use of deep destructuring assignment is nice here because the function's signature shows the shape of the data that interleave
is expecting; an array of arrays. Mathematical induction allows us to naturally handle the branches of our program -
const None =
Symbol ('None')
const interleave =
( [ [ v = None, ...vs ] = [] // first subarray
, ...rest // rest of subarrays
]
) =>
v === None
? rest.length === 0
? vs // base: no `v`, no `rest`
: interleave (rest) // inductive: some `rest`
: [ v, ...interleave ([ ...rest, vs ]) ] // inductive: some `v`, some `rest`
const input =
[ [ "one", "two", "three" ]
, [ "uno", "dos" ]
, [ "1", "2", "3", "4" ]
, [ "first", "second", "third" ]
]
console.log (interleave (input))
// [ "one", "uno", "1", "first", "two", "dos", "2", "second", "three", "3", "third", "4" ]
interleave
has released us from the shackles of close-minded thinking. I no longer need to think about my problem in terms of misshapen pieces that awkwardly fit together – I'm not thinking about array indexes, or sort
, or forEach
, or mutating state with push
, or making comparisons using >
or Math.max
. Nor am I having to think about perverse things like array-like – wow, we really do take for granted just how much we've come to know about JavaScript!
Above, it should feel refreshing that there are no dependencies. Imagine a beginner approaching this program: he/she would only need to learn 1) how to define a function, 2) destructuring syntax, 3) ternary expressions. Programs cobbled together with countless small dependencies will require the learner to familiarize him/herself with each before an intuition for the program can be acquired.
That said, JavaScript syntaxes for destructuring values are not the most pretty and sometimes trades for convenience are made for increased readability -
const interleave = ([ v, ...vs ], acc = []) =>
v === undefined
? acc
: isEmpty (v)
? interleave (vs, acc)
: interleave
( [ ...vs, tail (v) ]
, [ ...acc, head (v) ]
)
The dependencies that evolved here are isEmpty
, tail
, and head
-
const isEmpty = xs =>
xs.length === 0
const head = ([ x, ...xs ]) =>
x
const tail = ([ x, ...xs ]) =>
xs
Functionality is the same -
const input =
[ [ "one", "two", "three" ]
, [ "uno", "dos" ]
, [ "1", "2", "3", "4" ]
, [ "first", "second", "third" ]
]
console.log (interleave (input))
// [ "one", "uno", "1", "first", "two", "dos", "2", "second", "three", "3", "third", "4" ]
Verify the results in your own browser below -
const isEmpty = xs =>
xs.length === 0
const head = ([ x , ...xs ]) =>
x
const tail = ([ x , ...xs ]) =>
xs
const interleave = ([ v, ...vs ], acc = []) =>
v === undefined
? acc
: isEmpty (v)
? interleave (vs, acc)
: interleave
( [ ...vs, tail (v) ]
, [ ...acc, head (v) ]
)
const input =
[ [ "one", "two", "three" ]
, [ "uno", "dos" ]
, [ "1", "2", "3", "4" ]
, [ "first", "second", "third" ]
]
console.log (interleave (input))
// [ "one", "uno", "1", "first", "two", "dos", "2", "second", "three", "3", "third", "4" ]
If you start thinking about interleave
by using map
, filter
, and reduce
, then it's likely they will be a part of the final solution. If this is your approach, it should surprise you that map
, filter
, and reduce
are nowhere to be seen in the two programs in this answer. The lesson here is you become a prisoner to what you know. You sometimes need to forget map
and reduce
in order to observe that other problems have a unique nature and thus a common approach, although potentially valid, is not necessarily the best fit.