Transducers are all the rage lately. They allow you to efficiently compose
operations on a list without creating individual intermediate array (good, no garbage collection pauses).
If you look at the examples you might notice a weird thing: all array operations are implemented
using Array.prototype.reduce
rather than individual operations (like map
, some
, filter
).
Why is that?
Typically we use reduce
to produce single value from a given list. For example to compute
the sum of numbers we can write
1 | var numbers = [3, 1, 7]; |
The key to understanding why reduce
is so popular is to notice that the output value could
be anything: an integer (like a sum), or a boolean (like a flag), or even another array (for example filtered values).
In other words, every other list operation can be quickly implemented using reduce
!
reducing forEach
Let us start with a simple print each number feature. It can be implemented using Array.prototype.forEach
or reduce
1 | // need to print each number |
We can completely ignore the first argument in the reduce
callback, because we are only interested in the value
of each item x
.
refactor reduce to replace forEach
We can refactor the callback to forEach
and reduce any such transformation using
ignore-argument utility.
1 | function print(x) { |
reducing map
The second method we can implement using reduce
is Array.prototype.map
that creats new list by passing
each item through a callback. Let us double each number.
1 | var doubled = numbers.map(function (x) { |
The same feature can be quickly implemented using reduce
if we start from an empty array and push
each doubled number to it.
1 | var reducedDoubled = numbers.reduce(function (alreadyDoubled, x) { |
refactor reduce to replace map
Let us refactor the callback passed to the Array.prototype.map
method and see how to use the same callback
function inside reduce
1 | function by2(x) { |
To wrap this by2
function to be useful as a callback to reduce
we can create utility function
1 | function reduceMap(fn) { |
reducing filter
Let us take Array.prototype.filter
that creates a new array from items that return truthy value when
passed through the callback function.
1 | // filter < 5 numbers |
Same feature implemented using reduce
again returns a new array, just like map
example above.
1 | var reducedSmall = numbers.reduce(function (alreadySmall, x) { |
refactor reduce to replace filter
Similarly to the map
example, we can wrap the callback function to be usable inside reduce
1 | function reduceFilter(fn) { |
reducing some
Another operation that we can implement using reduce
is Array.prototype.some
that returns true
if there is an item in the list for which the callback returns truthy value
1 | // any number > 6? |
Same feature using reduce
has to go through each item (performance penalty), but can avoid
evaluating the callback using short-circuit evaluation
1 | console.log('reduced any number > 6?', numbers.reduce(function (found, x) { |
refactor reduce to replace some
Let us wrap any callback to some
to work for reduce
1 | function reduceSome(fn) { |
Bonus 1 - generalize each reduce transform
Let us take a look at each utility function that transforms a regular callback function into
reduce-compatible function. I even add reduceEach
that I implemented using ignore-argument
utility function above
1 | function reduceForEach(fn) { |
In all cases we maybe transform the previous value (called mapped
or found
) using
the callback applied to the item's value x
. Let us make each signature uniform,
placing the original function first (preparing it for partial application). I will
call these little functions reduce combinators.
1 | // forEach - the simplest case |
We can use this primitive combinators directly when calling reduce
. For example
here is the forEach
equivalent.
1 | function print(x) { |
Similarly, we can call other methods using Array.prototype.reduce
combinators
1 | console.log('numbers * 2', numbers.reduce( |
In each case we called numbers.reduce
with a different adaptor and starting value.
Bonus 2 - curry each reduce combinator
The same boilerplate code to partially apply the callback function using Function.prototype.bind
quickly becomes annoying. Let us apply the curry
method to each function each
, filter
, etc.
1 | var R = require('ramda'); |
Now we can conveniently wrap a callback function. All we need to remember is the starting value
1 | numbers.reduce(each(print), 0); |
Bonus 3 - remember the starting value
Remembering to pass 0
when using the each
reduce adaptor, or false
when using some
adaptor
quickly becomes a chore. Since functions in JavaScript are objects, we can store the initial value
as a property. For example
1 | var each = R.curry(function each(fn, prev, x) { |
then we don't have to remember how to start the reduce method
1 | numbers.reduce(each(print), each.start); |
Bonus 4 - on to transducers
Finally, why do we have to use the Array.prototype.reduce
at all? We have to
wrap the callback function and pass the starting value. We can create a new function that will take
care of the wrapping and starting automatically.
1 | // reducer function |
We are assuming that a combinator
is our curried function that can transform the given callback
and also has the start value. The reducer
is so general now that it is almost a transducer!
Conclusion
Array.prototype.reduce
is powerful, and every other Array method can be quickly reduced to using
this list processing method. This is why it forms the basis for other functional solutions.
This makes a good interview question too, wink, wink.
Bonus 5 - querySelectorAll
Imagine we want to select multiple elements in the HTML document. I usually start with a simple list of selectors and for each selector grab an element
1 | var selectors = ['#foo', '.bar'] |
The cardinality of selectors
is the same as elements
. What if each selector can
potentially return many items? Then we get a list of lists!
1 | var selectors = ['#foo', '.bar'] |
Oh, and by the way, document.querySelectorAll
returns
NodeList, not an Array. Luckily we can
easily convert it to an array using modern JavaScript.
1 | var selectors = ['#foo', '.bar'] |
We probably want to flatten the returned list of elements, yet JavaScript does not have a built-in
function to flatten arrays. We could use array concat
method, but this requires an external variable
1 | var selectors = ['#foo', '.bar'] |
Building a single data structure when iterating over the array like we are here is the
best use case for reduce
.
1 | var selectors = ['#foo', '.bar'] |
Notice that we no longer have an external variable, and the returned result elements
is
declared constant.