This was an explanation I gave to someone on Reddit (original question now deleted, thread here) regarding their frustrations extracting data using functional-style techniques (they were doing the twelfth functional programming exercise here).
var movieLists = [
{
name: "Instant Queue",
videos : [
{
"id": 70111470,
"title": "Die Hard",
"boxarts": [
{ width: 150, height: 200, url: "http://cdn-0.nflximg.com/images/2891/DieHard150.jpg" },
{ width: 200, height: 200, url: "http://cdn-0.nflximg.com/images/2891/DieHard200.jpg" }
],
"url": "http://api.netflix.com/catalog/titles/movies/70111470",
"rating": 4.0,
"bookmark": []
},
{
"id": 654356453,
"title": "Bad Boys",
"boxarts": [
{ width: 200, height: 200, url: "http://cdn-0.nflximg.com/images/2891/BadBoys200.jpg" },
{ width: 150, height: 200, url: "http://cdn-0.nflximg.com/images/2891/BadBoys150.jpg" }
],
"url": "http://api.netflix.com/catalog/titles/movies/70111470",
"rating": 5.0,
"bookmark": [{ id: 432534, time: 65876586 }]
}
]
},
{
name: "New Releases",
videos: [
{
"id": 65432445,
"title": "The Chamber",
"boxarts": [
{ width: 150, height: 200, url: "http://cdn-0.nflximg.com/images/2891/TheChamber150.jpg" },
{ width: 200, height: 200, url: "http://cdn-0.nflximg.com/images/2891/TheChamber200.jpg" }
],
"url": "http://api.netflix.com/catalog/titles/movies/70111470",
"rating": 4.0,
"bookmark": []
},
{
"id": 675465,
"title": "Fracture",
"boxarts": [
{ width: 200, height: 200, url: "http://cdn-0.nflximg.com/images/2891/Fracture200.jpg" },
{ width: 150, height: 200, url: "http://cdn-0.nflximg.com/images/2891/Fracture150.jpg" },
{ width: 300, height: 200, url: "http://cdn-0.nflximg.com/images/2891/Fracture300.jpg" }
],
"url": "http://api.netflix.com/catalog/titles/movies/70111470",
"rating": 5.0,
"bookmark": [{ id: 432534, time: 65876586 }]
}
]
}
];
Create a query that selects {id, title, boxart} for every video in the movieLists. The boxart property in the result will be the url of the boxart object with dimensions of 150x200px.
With functional styles, it’s important to think of a program written using functional techniques as "I have x data, which looks like this, and I would like to transform the data so that it looks like that. I know what the structure is now, and I know what I want the structure to look like, so I will pipe it through a series of transformation functions. Each one of those functions will create a new, reshaped version of the original data (at no point should the original data be mutated), and I can test, add, remove or alter each one of those transformation function in isolation.
This is a possible solution using ES6 syntax for brevity. Note that is isn't exactly what the description specified, as I use Array.prototype.find()
instead of Array.prototype.filter()
, and I just used Array.prototype.reduce()
instead of investigating how he'd used it to make a concatAll()
function:
movieLists.reduce((accumulatorArray, {videos}) => accumulatorArray.concat(videos), [])
.map(function({id, title, boxarts}) {
const bart = boxarts.find(({width, height}) => width === 150 && height === 200);
return { id: id, title: title, boxart: bart["url"] };
});
// replace the `return` line in map with this `JSON.stringify` version
// to see the output in FF's scratchpad rather than `[Object object]`
// return JSON.stringify({ id: id, title: title, boxart: bart });
- I have an array called movieLists that contains a series of objects.
- Each object has another array containing a series of objects representing videos.
- The required end product is just an array of objects containing specific informations about the videos.
- Therefore extract the arrays of videos and shove them into an array; I couldn't care less about anything else.
Array.prototype.reduce()
takes two arguments: the first is a callback function, the second is an initial value (which defaults to the first value in the array). The function must have [at the very least] two arguments: one being the previous value, one being the current value. So [1,2,3].reduce((prev, current) => prev + current)
would start at 1 (prev), and add 2 (current). Then it traverses right, so the prev value is now 3 (1+2), and it would add 3 (current). Now it hits the end of the array and stops, returning the result of 6. 1 + 2 then 3 + 3 then 6.
The second argument (the initial value) is left off in that example. If instead, the reduce looks like [1,2,3].reduce((prev, current) => prev + current, 10)
, the steps would be: 10 + 1 then 11 + 2 then 13 + 3 then 16.
The initial value can be anything. If it was to be an array, the function can collect things into that array. For example:
[["foo"],["bar"],["baz"]].reduce((prev, current) => prev.concat(current), [])
With that, the steps are:
[].concat(["foo"])
["foo"].concat(["bar"])
["foo", "bar"].concat(["baz"])
["foo", "bar", "baz"]
So, for the first transformation, I take the movieLists
array, and apply reduce. The initial value is an empty array. The callback function takes the accumulator array as the first parameter, and the object containing the videos
array as the second parameter. ES6 allows me to match on specific key/s from an object (I want the videos
array - but it could easily have looked like:
movieLists.reduce(function(accumulatorArray, movieList) {
return accumulatorArray.concat(movieList["videos"])
}, [])
So the sequence looks like:
[].concat([{video_info:"blah"}, {video_info:"blah"}])
[{video_info:"blah"}, {video_info:"blah"}].concat([{video_info:"blah"}, {video_info:"blah"}])
[{video_info:"blah"}, {video_info:"blah"}, {video_info:"blah"}, {video_info:"blah"}]
I can now map over the array I have just created and extract the values I want (building a new array that contains only what I want). I can just chain map()
straight onto the end of the reduce()
function: I want to transform the value reduce()
returned. As that was an array, map()
will work just fine.
The array is a collection of objects, and I want to select the values I want, and returning a new array of objects with only this information.
The simplest way to get them is to return
an object literal populated with the values I require. Again, I’ve used the syntax available in ES6 to simplify this and specify exactly which object keys I want, but it can be written like this:
.map(function(film) {
var id = film["id"];
var title = film["title"];
var boxart = film["boxarts"];
return { id: id, title: title, boxart: boxart };
});
This bit is not quite finished - boxarts
is an array, and I only need one value from one specific object in that array. I can still make use of the variable boxart
, but I need to specify exactly what it is. The method I used, Array.prototype.find()
returns a specific value, which is ideal in this case, but again, is an ES6 array addition. I could do something like this, make a filter designed to only return a single-item array, and grab the value I want from that item:
var boxart = film["boxarts"].filter(function(bart) {
return bart["width"] === 150 && bart["height"] === 200;
})[0]["url"];