Last reviewed in June 2019
Fold is a common higher-order function widely used in functional programming. In Javascript, we have an analogous function called reduce.
While not specific to Ember, reduce
is an amazingly helpful tool. It is so versatile that it allows us to transform data structures in virtually any way we want.
This is the anatomy of a reduce
:
array.reduce((acc, value, index, array) => {
// ...
return acc;
}, initialValue);
The supplied function will be called once per element in the array
:
value
is the content of the current element of the array
, and index
… well, its indexacc
is what I call the “accumulator”. It is the object that holds state across iterations. For the first element of the array
, acc
will take the value supplied in initialValue
. For the remaining elements, it will take the value returned in the previous invocation of the function.It can sum an array (from Array
to Number
):
[1, 2, 3].reduce((acc, value) => {
return acc + value;
}, 0);
// => 6
or find a max:
[1, 2, 7, 3].reduce((acc, value) => {
if (acc < value) acc = value;
return acc;
}, -Infinity);
// => 7
It can flatten nested objects (from Array
or Object
to Array
):
const nested = {
id: 1,
children: [
{ id: 2 },
{ id: 3,
children: [{ id: 5 }, { id: 6 }]
},
{ id: 4 }
]
}
const flatten = (obj) => {
const array = Array.isArray(obj) ? obj : [obj];
return array.reduce((acc, value) => {
acc.push(value);
if (value.children) {
acc = acc.concat(flatten(value.children));
delete value.children;
}
return acc;
}, []);
}
flatten(nested);
// => [ { id: 1 }, { id: 2 }, { id: 3 }, { id: 5 }, { id: 6 }, { id: 4 } ]
It can implement map
:
const map = (array, fn) => {
return array.reduce((acc, value) => {
value = fn.call(this, value);
acc.push(value);
return acc;
}, []);
}
// add 1 to each number
map([1, 2], n => n + 1);
// => [2, 3]
…or filter
:
const filter = (array, fn) => {
return array.reduce((acc, value) => {
const ok = fn.call(this, value);
if (!!ok) acc.push(value);
return acc;
}, []);
}
// return only even numbers
filter([1, 2, 5, 7, 8, 10, 104, 1189], n => n % 2 === 0);
// => [ 2, 8, 10, 104 ]
It can join strings:
["hello", "this", "is", "awesome"].reduce((acc, value) => {
if (acc) acc = acc.concat(" ");
acc = acc.concat(value);
return acc;
}, "");
// => 'hello this is awesome'
And even call promises serially:
const models = [model1, model2, model3];
models.reduce((previous, model) => {
return previous.then(() => model.save());
}, RSVP.resolve());
Say we have a template displaying a list of spare parts (each with its name
, sku
and amountInStock
).
In the component (or controller) that backs that template we can define totalAmountInStock
:
totalAmountInStock: computed('parts.@each.amountInStock', function() {
return this.parts.reduce((acc, value) => {
return acc + value.amountInStock;
}, 0);
});
// and then {{totalAmountInStock}} in the template!
The total amount gets effortlessly updated!
(Yes, I know this can be done with computed.sum
… but hey, I needed an easy example!)
To sum up, no pun intended, whenever you need to transform data (even from one data type to another) there is one tool that will always come handy: that is reduce!
Snacks is the best of Ember Octane in a highly digestible monthly newsletter. (No spam. EVER.)