Problems and reducers
We want to display a video in a web application. We have the video in different formats and resolutions. The place to put the video in changes size depending on the device and orientation.
We want to use the only mp4 format and download the video with bigger than the space of but not more. In other words: we want to optimize the downloading.
Question: how to find the correct video from the list using a reducer?
Working with arrays in JS #
Compared to other languages (Ruby, I'm looking at you) JS have only a few methods to manipulate arrays. The basics are:
- filter: select some elements based on it's characteristics
- find: select one element
- sort: it do what it says
- slice, splice: cut the array in segments
- reduce: make anything of above and more
But I'd say that most of the time, using filter, find and sort is enough.
Solve the problem, the easy way #
In our case, what we want:
- remove all formats but mp4
- remove all formats smaller than our video size
- sort ordered by size
- pick the first
In plain JS:
const source = sources
.filter(x => x.format === 'mp4')
.filter(x => x.width > elementWidth)
.sort((a, b) => a.width - b.width)
[0];
How to use a reducer #
Ok. As I written before, reducer is the "mother" of all functions, because every other can be implemented with reducer.
The reducer function receives two parameters. First is expected output (normally called "accumulator") and second is the current item of the array.
The return value from the reducer in one iteration, will become the accumulator parameter in the next iteration.
The "hello world" of the reducers is "sum":
const sum = [1, 2, 3, 4].reduce(
(acc, num) => acc + num), // <= the reducer
0 // <= the initial value of the accumulator
)
sum // => 10
Another canonical example is to convert an array into an object. In this case, we are returning the same result on every iteration, so the accumulator will be always the same:
const array = ['a', 'b', 'c']
const object = array.reduce((acc, item) => {
acc[item] = true;
return acc;
}, {}) // <= this second parameter {} is the accumulator
object // => { a: true, b: true, c: true }
Last but not least, in Javascript, if we don't specify an initial value of the accumulator, first first iteration of the reductor will contain the first and second element of the array.
For example, the following code:
const array = [1, 2, 3, 4]
const result = array.reduce((acc, item) => {
console.log({ acc, item });
return item;
})
will output:
{ acc: 1, item: 2 } // <= first two items of the array
{ acc: 2, item: 3 } // <= we returned the item from last iteration
{ acc: 3, item: 4 } // <= same
and the result will be 4
.
The good thing of a reducer is that it can combine all other operations into one, as we will see in our example.
The solution #
Ok, maybe it was a too long introduction to reducers. Here's the solution:
sources.reduce((selected, item) => {
return (item.format === 'mp4' &&
item.width > elementWidth &&
item.width < selected.width) ? item : selected;
})
What do you prefer? The "easy" or the "reducer" solution?
The real solution #
Spoiler: don't solve the problem, destroy it.
I really think we, as application developers, shouldn't do this kind of optimizations. This is something the browser should do it by default (the same way it does for images, for example).
In fact, I think this kind of optimization could bring more problems than solutions. For example, what happens if the device size changes? Or if some one video is already downloaded (and cached) and we force to download another (even if it's smaller)?
I mean: only browser vendor are able to solve this kind of problems because they have all the information required to solve it (like cache and network status, device capabilities, common usage patterns, etc).
We should focus on solve our own (application domain) problems.
In this specific example, I don't think the optimization is relevant because, for most of our users, the mobile network nowadays works as well (if not better) than the wired network. And there are lot of issues to solve in order to optimize correctly.
My preference #
If you are wonder, I prefer the easy way to solve the problem (using filter, find and sort) than the reducer version. Basically two reasons:
- Easy to read and understand: It's impossible to know what a reducer does without looking into the reducer code. The easy it's much easier to grasp: filter, sort and select first.
- Composability: The reducers are not composable (or at least, not all reducers). That means that you can't (always) combine them to build complex logic. Most of the other array methods are composable by default (like in our example, where we "chained" one into another)
- Easy to change: if the algorithm changes (for example, we want to use any format) the "easy" version is much easier to modify than the "reducer" one (and the more complex the business logic, the more difficult it is to modify a reducer)
Conclusion #
As a conclusion (what we all already know in theory, but which is much more difficult to apply in practice):
- Prefer filter, and sort over reducer
- Prefer easy vs complex (aka: don't try to be smart)
- Don't do premature optimization
- (Try to) don't do optimization at all :-)