This is an interactive learning course with exercises you fill out right in the browser. If you just want to browse the content click the button below: Show all the answers so I can just browse.

Functional Programming in Javascript

This is a series of interactive exercises for learning Microsoft's Reactive Extensions (Rx) Library for Javascript. So why is the title "Functional Programming in Javascript"? Well it turns out that the key to learning Rx is training yourself to use functional programming to manipulate collections. Functional programming provides developers with the tools to abstract common collection operations into reusable, composable building blocks. You'll be surprised to learn that most of the operations you perform on collections can be accomplished with five simple functions (some native to JavaScript and some included in the RxJS library):

map filter concatAll reduce zip

Here's my promise to you: if you learn these 5 functions your code will become shorter, more self-descriptive, and more durable. Also, for reasons that might not be obvious right now, you'll learn that these five functions hold the key to simplifying asynchronous programming. Once you've finished this tutorial you'll also have all the tools you need to easily avoid race conditions, propagate and handle asynchronous errors, and sequence events and AJAX requests. In short, these 5 functions will probably be the most powerful, flexible, and useful functions you'll ever learn.

Finishing the Interactive Exercises

This isn't just a tutorial, it's a series of interactive exercises that you can fill out right in your browser! It's easy to finish the exercises. Just edit the code and press "Run." If the code works, a new exercise will appear below. Otherwise an error will appear.

Note: Use the "F4" key to toggle full screen mode for each editor.

This tutorial may have bugs, so if you get into a weird state or you're sure you've got the right answer but can't move on, just refresh your browser. If you're using a modern browser, and if you're here I assume you are, the state of your exercises will be saved. If you want, you can also restart the lab.

This tutorial is on GitHub, and is asymptotically approaching completion. If you'd like to add an exercise, clarify a problem description, or fix a bug feel free to fork and send us a pull request. We'll try and fit user-contributed exercises into a narrative.

Your answers will be saved in local storage. Use the buttons below if you want to transfer them to another machine: Get Answers as JSON Set JSON Answers

Working with Arrays

The Array is Javascript's only collection type. Arrays are everywhere. We're going to add the five functions to the Array type, and in the process make it much more powerful and useful. As a matter of fact, Array already has the map, filter, and reduce functions! However we're going to reimplement these functions as a learning exercise.

This section will follow a pattern. First we'll solve problems the way you probably learned in school, or on your own by reading other people's code. In other words, we'll transform collections into new collections using loops and statements. Then we'll implement one of the five functions, and then use it to solve the same problem again without the loop. Once we've learned the five functions, you'll learn how to combine them to solve complex problems with very little code. The first two exercises have been completed in advance, but please look them over carefully!

Traversing an Array

Exercise 1: Print all the names in an array function(console) { var names = ["Ben", "Jafar", "Matt", "Priya", "Brian"], counter; for(counter = 0; counter < names.length; counter++) { console.log(names[counter]); } } Run <- Click here to try your solution. If it works, you'll move on to the next exercise. // Traverse array with for loop function(str) { preVerifierHook(); var fun = eval("(" + str + ")"); var items = []; var got; var expected = '["Ben","Brian","Jafar","Matt","Priya"]'; fun({ log:function(name) { items.push(name); console.log(name); } }); got = JSON.stringify(items.sort()); if(got === expected) { return "Success!" } else { showLessonErrorMessage(expected, got, 'Note: order does not matter'); } } function(console) { var names = ["Ben", "Jafar", "Matt", "Priya", "Brian"], counter; for(counter = 0; counter < names.length; counter++) { console.log(names[counter]); } } Ask yourself this question: did we need to specify the order in which the names were printed? If not, why do it?

Exercise 2: Use forEach to print all the names in an array Let's repeat the previous exercise using the forEach function. function(console) { var names = ["Ben", "Jafar", "Matt", "Priya", "Brian"]; names.forEach(function(name) { console.log(name); }); } Run // Traverse array with foreach function(str) { preVerifierHook(); if (str.indexOf(".forEach") === -1) { return "You have to use forEach!" } var fun = eval("(" + str + ")"); var items =[]; fun({ log:function(name) { items.push(name); console.log(name); } }); if(JSON.stringify(items.sort()) === '["Ben","Brian","Jafar","Matt","Priya"]') { return "Success!" } else { throw 'console.log did not receive all of these values: "Ben","Brian","Jafar","Matt","Priya" (note: order does not matter)' } } function(console) { var names = ["Ben", "Jafar", "Matt", "Priya", "Brian"]; names.forEach(function(name) { console.log(name); }); } Notice that forEach lets us specify what we want to happen to each item in the array, but hides how the array is traversed. Projecting Arrays Applying a function to a value and creating a new value is called a projection. To project one array into another, we apply a projection function to each item in the array and collect the results in a new array.

Exercise 3: Project an array of videos into an array of {id,title} pairs using forEach() For each video, add a projected {id, title} pair to the videoAndTitlePairs array. function() { var newReleases = [ { "id": 70111470, "title": "Die Hard", "boxart": "http://cdn-0.nflximg.com/images/2891/DieHard.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": [4.0], "bookmark": [] }, { "id": 654356453, "title": "Bad Boys", "boxart": "http://cdn-0.nflximg.com/images/2891/BadBoys.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": [5.0], "bookmark": [{ id: 432534, time: 65876586 }] }, { "id": 65432445, "title": "The Chamber", "boxart": "http://cdn-0.nflximg.com/images/2891/TheChamber.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": [4.0], "bookmark": [] }, { "id": 675465, "title": "Fracture", "boxart": "http://cdn-0.nflximg.com/images/2891/Fracture.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": [5.0], "bookmark": [{ id: 432534, time: 65876586 }] } ], videoAndTitlePairs = []; // ------------ INSERT CODE HERE! ----------------------------------- // Use forEach function to accumulate {id, title} pairs from each video. // Put the results into the videoAndTitlePairs array using the Array's // push() method. Example: videoAndTitlePairs.push(newItem); // ------------ INSERT CODE HERE! ----------------------------------- return videoAndTitlePairs; } Run Show Answer Enter Fullscreen // Projection with with forEach function(str) { preVerifierHook(); var fun = eval("(" + str + ")"), videoAndTitlePairs = fun(), expected = '[{\"id\":675465,\"title\":\"Fracture\"},{\"id\":65432445,\"title\":\"The Chamber\"},{\"id\":70111470,\"title\":\"Die Hard\"},{\"id\":654356453,\"title\":\"Bad Boys\"}]'; // Sorting by video id videoAndTitlePairs = videoAndTitlePairs.sortBy(function(video) { return video.id }); if (JSON.stringify(videoAndTitlePairs) === expected) { return true; } else { showLessonErrorMessage(expected, JSON.stringify(videoAndTitlePairs)); } } function() { var newReleases = [ { "id": 70111470, "title": "Die Hard", "boxart": "http://cdn-0.nflximg.com/images/2891/DieHard.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": [4.0], "bookmark": [] }, { "id": 654356453, "title": "Bad Boys", "boxart": "http://cdn-0.nflximg.com/images/2891/BadBoys.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": [5.0], "bookmark": [{ id: 432534, time: 65876586 }] }, { "id": 65432445, "title": "The Chamber", "boxart": "http://cdn-0.nflximg.com/images/2891/TheChamber.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": [4.0], "bookmark": [] }, { "id": 675465, "title": "Fracture", "boxart": "http://cdn-0.nflximg.com/images/2891/Fracture.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": [5.0], "bookmark": [{ id: 432534, time: 65876586 }] } ], videoAndTitlePairs = []; newReleases.forEach(function(video) { videoAndTitlePairs.push({ id: video.id, title: video.title }); }); return videoAndTitlePairs; } All array projections share two operations in common: Traverse the source array Add each item's projected value to a new array Why not abstract away how these operations are carried out?

Exercise 4: Implement map() To make projections easier, let's add a map() function to the Array type. Map accepts the projection function to be applied to each item in the source array, and returns the projected array. Array.prototype.map = function(projectionFunction) { var results = []; this.forEach(function(itemInArray) { // ------------ INSERT CODE HERE! ---------------------------- // Apply the projectionFunction to each item in the array and add // each result to the results array. // Note: you can add items to an array with the push() method. // ------------ INSERT CODE HERE! ---------------------------- }); return results; }; // JSON.stringify([1,2,3].map(function(x) { return x + 1; })) === '[2,3,4]' Run Show Answer Enter Fullscreen // Implement map() function(str) { preVerifierHook(); var fun = eval(str), arr = [1,2,3], result; result = arr.map(function(x) { return x + 1}); if (JSON.stringify(arr) !== "[1,2,3]") { throw "Whoa! You changed the input array. Map never changes the value of the array passed in. It creates a new array with the results of applying the projection function to every value in the old array." } else if(JSON.stringify(result) !== '[2,3,4]') { throw 'Expected that [1,2,3].map(function(x) { return x + 1}) would equal [2,3,4].' } } Array.prototype.map = function(projectionFunction) { var results = []; this.forEach(function(itemInArray) { results.push(projectionFunction(itemInArray)); }); return results; }; // JSON.stringify([1,2,3].map(function(x) { return x + 1; })) === '[2,3,4]'

Exercise 5: Use map() to project an array of videos into an array of {id,title} pairs Let's repeat the exercise of collecting {id, title} pairs for each video in the newReleases array, but this time we'll use our map function. function() { var newReleases = [ { "id": 70111470, "title": "Die Hard", "boxart": "http://cdn-0.nflximg.com/images/2891/DieHard.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": [4.0], "bookmark": [] }, { "id": 654356453, "title": "Bad Boys", "boxart": "http://cdn-0.nflximg.com/images/2891/BadBoys.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": [5.0], "bookmark": [{ id: 432534, time: 65876586 }] }, { "id": 65432445, "title": "The Chamber", "boxart": "http://cdn-0.nflximg.com/images/2891/TheChamber.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": [4.0], "bookmark": [] }, { "id": 675465, "title": "Fracture", "boxart": "http://cdn-0.nflximg.com/images/2891/Fracture.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": [5.0], "bookmark": [{ id: 432534, time: 65876586 }] } ]; // ------------ INSERT CODE HERE! ----------------------------------- // Use map function to accumulate {id, title} pairs from each video. return newReleases.map // finish this expression! // ------------ INSERT CODE HERE! ----------------------------------- } Run Show Answer Enter Fullscreen // Projection with map function(str) { preVerifierHook(); var fun = eval("(" + str + ")"), videoAndTitlePairs = fun(), expected = '[{\"id\":675465,\"title\":\"Fracture\"},{\"id\":65432445,\"title\":\"The Chamber\"},{\"id\":70111470,\"title\":\"Die Hard\"},{\"id\":654356453,\"title\":\"Bad Boys\"}]'; // Sorting by video id videoAndTitlePairs = videoAndTitlePairs.sortBy(function(video) { return video.id }); if (JSON.stringify(videoAndTitlePairs) === expected) { return true; } else { throw 'Expected: ' + expected; } } function() { var newReleases = [ { "id": 70111470, "title": "Die Hard", "boxart": "http://cdn-0.nflximg.com/images/2891/DieHard.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": [4.0], "bookmark": [] }, { "id": 654356453, "title": "Bad Boys", "boxart": "http://cdn-0.nflximg.com/images/2891/BadBoys.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": [5.0], "bookmark": [{ id: 432534, time: 65876586 }] }, { "id": 65432445, "title": "The Chamber", "boxart": "http://cdn-0.nflximg.com/images/2891/TheChamber.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": [4.0], "bookmark": [] }, { "id": 675465, "title": "Fracture", "boxart": "http://cdn-0.nflximg.com/images/2891/Fracture.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": [5.0], "bookmark": [{ id: 432534, time: 65876586 }] } ]; return newReleases.map(function(video) { return { id: video.id, title: video.title }; }); } Notice that map allows us to specify what projection we want to apply to an array, but hides how the operation is carried out. Filtering Arrays Like projection, filtering an array is also a very common operation. To filter an array we apply a test to each item in the array and collect the items that pass into a new array.

Exercise 6: Use forEach() to collect only those videos with a rating of 5.0 Use forEach() to loop through the videos in the newReleases array and, if a video has a rating of 5.0, add it to the videos array. function() { var newReleases = [ { "id": 70111470, "title": "Die Hard", "boxart": "http://cdn-0.nflximg.com/images/2891/DieHard.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 4.0, "bookmark": [] }, { "id": 654356453, "title": "Bad Boys", "boxart": "http://cdn-0.nflximg.com/images/2891/BadBoys.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 5.0, "bookmark": [{ id: 432534, time: 65876586 }] }, { "id": 65432445, "title": "The Chamber", "boxart": "http://cdn-0.nflximg.com/images/2891/TheChamber.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 4.0, "bookmark": [] }, { "id": 675465, "title": "Fracture", "boxart": "http://cdn-0.nflximg.com/images/2891/Fracture.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 5.0, "bookmark": [{ id: 432534, time: 65876586 }] } ], videos = []; // ------------ INSERT CODE HERE! ----------------------------------- // Use forEach function to accumulate every video with a rating of 5.0 // ------------ INSERT CODE HERE! ----------------------------------- return videos; } Run Show Answer Enter Fullscreen // Filter with forEach function(str) { preVerifierHook(); var fun = eval("(" + str + ")"), videos = fun(), expected = '[{"id":675465,"title":"Fracture","boxart":"http://cdn-0.nflximg.com/images/2891/Fracture.jpg","uri":"http://api.netflix.com/catalog/titles/movies/70111470","rating":5,"bookmark":[{"id":432534,"time":65876586}]},{"id":654356453,"title":"Bad Boys","boxart":"http://cdn-0.nflximg.com/images/2891/BadBoys.jpg","uri":"http://api.netflix.com/catalog/titles/movies/70111470","rating":5,"bookmark":[{"id":432534,"time":65876586}]}]'; // Sorting by video id videos = videos.sortBy(function(v) { return v.id; }); if (JSON.stringify(videos) === expected) { return true; } else { throw 'Expected: ' + expected; } } function() { var newReleases = [ { "id": 70111470, "title": "Die Hard", "boxart": "http://cdn-0.nflximg.com/images/2891/DieHard.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 4.0, "bookmark": [] }, { "id": 654356453, "title": "Bad Boys", "boxart": "http://cdn-0.nflximg.com/images/2891/BadBoys.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 5.0, "bookmark": [{ id: 432534, time: 65876586 }] }, { "id": 65432445, "title": "The Chamber", "boxart": "http://cdn-0.nflximg.com/images/2891/TheChamber.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 4.0, "bookmark": [] }, { "id": 675465, "title": "Fracture", "boxart": "http://cdn-0.nflximg.com/images/2891/Fracture.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 5.0, "bookmark": [{ id: 432534, time: 65876586 }] } ], videos = []; newReleases.forEach(function(video) { if (video.rating === 5.0) { videos.push(video); } }); return videos; } Notice that, like map(), every filter() operation shares some operations in common: Traverse the array Add objects that pass the test to a new array Why not abstract away how these operations are carried out?

Exercise 7: Implement filter() To make filtering easier, let's add a filter() function to the Array type. The filter() function accepts a predicate. A predicate is a function that accepts an item in the array, and returns a boolean indicating whether the item should be retained in the new array. Array.prototype.filter = function(predicateFunction) { var results = []; this.forEach(function(itemInArray) { // ------------ INSERT CODE HERE! ---------------------------- // Apply the predicateFunction to each item in the array. // If the result is truthy, add the item to the results array. // Note: remember you can add items to the array using the array's // push() method. // ------------ INSERT CODE HERE! ---------------------------- }); return results; }; // JSON.stringify([1,2,3].filter(function(x) { return x > 2})) === "[3]" Run Show Answer Enter Fullscreen // Implement filter() function(str) { preVerifierHook(); var fun = eval(str), arr = [1,2,3], result; result = arr.filter(function(x) { return x > 2}); if (JSON.stringify(arr) !== "[1,2,3]") { throw "Whoa! You changed the input array. Filter never changes the value of the array passed in. It creates a new array that includes only those items in the old array that pass the predicate function." } else if(JSON.stringify(result) !== '[3]') { throw 'Expected that [1,2,3].filter(function(x) { return x > 2}) would equal [3].' } } Array.prototype.filter = function(predicateFunction) { var results = []; this.forEach(function(itemInArray) { if (predicateFunction(itemInArray)) { results.push(itemInArray); } }); return results; }; // JSON.stringify([1,2,3].filter(function(x) { return x > 2})) === "[3]" Like map(), filter() lets us express what data we want without requiring us to specify how we want to collect the data.

Query Data by Chaining Method Calls Exercise 8: Chain filter and map to collect the ids of videos that have a rating of 5.0 function() { var newReleases = [ { "id": 70111470, "title": "Die Hard", "boxart": "http://cdn-0.nflximg.com/images/2891/DieHard.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 4.0, "bookmark": [] }, { "id": 654356453, "title": "Bad Boys", "boxart": "http://cdn-0.nflximg.com/images/2891/BadBoys.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 5.0, "bookmark": [{ id: 432534, time: 65876586 }] }, { "id": 65432445, "title": "The Chamber", "boxart": "http://cdn-0.nflximg.com/images/2891/TheChamber.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 4.0, "bookmark": [] }, { "id": 675465, "title": "Fracture", "boxart": "http://cdn-0.nflximg.com/images/2891/Fracture.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 5.0, "bookmark": [{ id: 432534, time: 65876586 }] } ]; // ------------ INSERT CODE HERE! ----------------------------------- // Chain the filter and map functions to select the id of all videos // with a rating of 5.0. return newReleases // Complete this expression // ------------ INSERT CODE HERE! ----------------------------------- } Run Show Answer Enter Fullscreen // Filter with filter() function(str) { preVerifierHook(); var fun = eval("(" + str + ")"), videoids = fun(), expected = '[675465,654356453]'; // Sorting by video id videoids = videoids.sortBy(function(v) { return v; }); if (JSON.stringify(videoids) === expected) { return true; } else { throw 'Expected: ' + expected; } } function() { var newReleases = [ { "id": 70111470, "title": "Die Hard", "boxart": "http://cdn-0.nflximg.com/images/2891/DieHard.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 4.0, "bookmark": [] }, { "id": 654356453, "title": "Bad Boys", "boxart": "http://cdn-0.nflximg.com/images/2891/BadBoys.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 5.0, "bookmark": [{ id: 432534, time: 65876586 }] }, { "id": 65432445, "title": "The Chamber", "boxart": "http://cdn-0.nflximg.com/images/2891/TheChamber.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 4.0, "bookmark": [] }, { "id": 675465, "title": "Fracture", "boxart": "http://cdn-0.nflximg.com/images/2891/Fracture.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 5.0, "bookmark": [{ id: 432534, time: 65876586 }] } ]; return newReleases. filter(function(video) { return video.rating === 5.0; }). map(function(video) { return video.id; }); } Chaining together map() and filter() gives us a lot of expressive power. These high level functions let us express what data we want, but leave the underlying libraries a great deal of flexibility in terms of how our queries are executed.

Querying Trees Sometimes, in addition to flat arrays, we need to query trees. Trees pose a challenge because we need to flatten them into arrays in order to apply filter() and map() operations on them. In this section we'll define a concatAll() function that we can combine with map() and filter() to query trees. Exercise 9: Flatten the movieLists array into an array of video ids Let's start by using two nested forEach loops to collect the id of every video in the two-dimensional movieLists array. function() { var movieLists = [ { name: "New Releases", videos: [ { "id": 70111470, "title": "Die Hard", "boxart": "http://cdn-0.nflximg.com/images/2891/DieHard.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 4.0, "bookmark": [] }, { "id": 654356453, "title": "Bad Boys", "boxart": "http://cdn-0.nflximg.com/images/2891/BadBoys.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 5.0, "bookmark": [{ id: 432534, time: 65876586 }] } ] }, { name: "Dramas", videos: [ { "id": 65432445, "title": "The Chamber", "boxart": "http://cdn-0.nflximg.com/images/2891/TheChamber.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 4.0, "bookmark": [] }, { "id": 675465, "title": "Fracture", "boxart": "http://cdn-0.nflximg.com/images/2891/Fracture.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 5.0, "bookmark": [{ id: 432534, time: 65876586 }] } ] } ], allVideoIdsInMovieLists = []; // ------------ INSERT CODE HERE! ----------------------------------- // Use two nested forEach loops to flatten the movieLists into a list of // video ids. // ------------ INSERT CODE HERE! ----------------------------------- return allVideoIdsInMovieLists; } Run Show Answer Enter Fullscreen // Use filter and map to collect video ids with rating of 5.0 function(str) { var fun = eval("(" + str + ")"), videos = fun(), expected = '[675465,65432445,70111470,654356453]'; videos = videos.sortBy(function(v) { return v }); if (JSON.stringify(videos) !== expected) { throw "Expected " + expected; } } function() { var movieLists = [ { name: "New Releases", videos: [ { "id": 70111470, "title": "Die Hard", "boxart": "http://cdn-0.nflximg.com/images/2891/DieHard.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 4.0, "bookmark": [] }, { "id": 654356453, "title": "Bad Boys", "boxart": "http://cdn-0.nflximg.com/images/2891/BadBoys.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 5.0, "bookmark": [{ id: 432534, time: 65876586 }] } ] }, { name: "Dramas", videos: [ { "id": 65432445, "title": "The Chamber", "boxart": "http://cdn-0.nflximg.com/images/2891/TheChamber.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 4.0, "bookmark": [] }, { "id": 675465, "title": "Fracture", "boxart": "http://cdn-0.nflximg.com/images/2891/Fracture.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 5.0, "bookmark": [{ id: 432534, time: 65876586 }] } ] } ], allVideoIdsInMovieLists = []; movieLists.forEach(function(movieList) { movieList.videos.forEach(function(video) { allVideoIdsInMovieLists.push(video.id); }); }); return allVideoIdsInMovieLists; } Flattening trees with nested forEach expressions is easy because we can explicitly add items to the array. Unfortunately it's exactly this type of low-level operation that we've been trying to abstract away with functions like map() and filter(). Can we define a function that's abstract enough to express our intent to flatten a tree, without specifying too much information about how to carry out the operation?

Exercise 10: Implement concatAll() Let's add a concatAll() function to the Array type. The concatAll() function iterates over each sub-array in the array and collects the results in a new, flat array. Notice that the concatAll() function expects each item in the array to be another array. Array.prototype.concatAll = function() { var results = []; this.forEach(function(subArray) { // ------------ INSERT CODE HERE! ---------------------------- // Add all the items in each subArray to the results array. // ------------ INSERT CODE HERE! ---------------------------- }); return results; }; // JSON.stringify([ [1,2,3], [4,5,6], [7,8,9] ].concatAll()) === "[1,2,3,4,5,6,7,8,9]" // [1,2,3].concatAll(); // throws an error because this is a one-dimensional array Run Show Answer Enter Fullscreen // Flatten movieLists into an array of video ids function(str) { preVerifierHook(); var fun = eval(str), arr = [[1,2,3],[4,5,6],[7,8,9]], result, expected = "[1,2,3,4,5,6,7,8,9]"; result = arr.concatAll(); result = result.sortBy(function(x) { return x; }); if (JSON.stringify(result) !== expected) { throw 'Expected that [[1,2,3],[4,5,6],[7,8,9]].concatAll() would equal [1,2,3,4,5,6,7,8,9].' } } Array.prototype.concatAll = function() { var results = []; this.forEach(function(subArray) { results.push.apply(results, subArray); }); return results; }; // JSON.stringify([ [1,2,3], [4,5,6], [7,8,9] ].concatAll()) === "[1,2,3,4,5,6,7,8,9]" // [1,2,3].concatAll(); // throws an error because this is a one-dimensional array concatAll is a very simple function, so much so that it may not be obvious yet how it can be combined with map() to query a tree. Let's try an example...

Exercise 11: Use map() and concatAll() to project and flatten the movieLists into an array of video ids Hint: use two nested calls to map() and one call to concatAll(). function() { var movieLists = [ { name: "New Releases", videos: [ { "id": 70111470, "title": "Die Hard", "boxart": "http://cdn-0.nflximg.com/images/2891/DieHard.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 4.0, "bookmark": [] }, { "id": 654356453, "title": "Bad Boys", "boxart": "http://cdn-0.nflximg.com/images/2891/BadBoys.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 5.0, "bookmark": [{ id: 432534, time: 65876586 }] } ] }, { name: "Dramas", videos: [ { "id": 65432445, "title": "The Chamber", "boxart": "http://cdn-0.nflximg.com/images/2891/TheChamber.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 4.0, "bookmark": [] }, { "id": 675465, "title": "Fracture", "boxart": "http://cdn-0.nflximg.com/images/2891/Fracture.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 5.0, "bookmark": [{ id: 432534, time: 65876586 }] } ] } ]; // ------------ INSERT CODE HERE! ----------------------------------- // Use map and concatAll to flatten the movieLists in a list of video ids. // ------------ INSERT CODE HERE! ----------------------------------- return movieLists // Complete this expression! } Run Show Answer Enter Fullscreen function(str) { preVerifierHook(); var fun = eval("(" + str + ")"), videos = fun(), expected = '[675465,65432445,70111470,654356453]'; videos = videos.sortBy(function(v) { return v }); if (JSON.stringify(videos) !== expected) { throw "Expected " + expected + "



Received " + JSON.stringify(videos); } } function() { var movieLists = [ { name: "New Releases", videos: [ { "id": 70111470, "title": "Die Hard", "boxart": "http://cdn-0.nflximg.com/images/2891/DieHard.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 4.0, "bookmark": [] }, { "id": 654356453, "title": "Bad Boys", "boxart": "http://cdn-0.nflximg.com/images/2891/BadBoys.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 5.0, "bookmark": [{ id: 432534, time: 65876586 }] } ] }, { name: "Dramas", videos: [ { "id": 65432445, "title": "The Chamber", "boxart": "http://cdn-0.nflximg.com/images/2891/TheChamber.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 4.0, "bookmark": [] }, { "id": 675465, "title": "Fracture", "boxart": "http://cdn-0.nflximg.com/images/2891/Fracture.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 5.0, "bookmark": [{ id: 432534, time: 65876586 }] } ] } ]; return movieLists. map(function(movieList) { return movieList.videos.map(function(video) { return video.id; }); }). concatAll(); } Wow! Great work. Mastering the combination of map() and concatAll() is key to effective functional programming. You're half way there! Let's try a more complicated example...

Exercise 12: Retrieve id, title, and a 150x200 box art url for every video You've managed to flatten a tree that's two levels deep, let's try for three! Let's say that instead of a single boxart url on each video, we had a collection of boxart objects, each with a different size and url. Create a query that selects {id, title, boxart} for every video in the movieLists. This time though, the boxart property in the result will be the url of the boxart object with dimensions of 150x200px. Let's see if you can solve this problem with map(), concatAll(), and filter(). There's just more one thing: you can't use indexers. In other words, this is illegal: var itemInArray = movieLists [0] ; Furthermore, you're not allowed to use indexers in any of the remaining exercises unless you're implementing one of the five functions. There is a very good reason for this restriction, and that reason will eventually be explained. For now, you'll simply have to accept it on faith that this restriction serves a purpose. :-) function() { 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 }] } ] } ]; // Use one or more map, concatAll, and filter calls to create an array with the following items // [ // {"id": 675465,"title": "Fracture","boxart":"http://cdn-0.nflximg.com/images/2891/Fracture150.jpg" }, // {"id": 65432445,"title": "The Chamber","boxart":"http://cdn-0.nflximg.com/images/2891/TheChamber150.jpg" }, // {"id": 654356453,"title": "Bad Boys","boxart":"http://cdn-0.nflximg.com/images/2891/BadBoys150.jpg" }, // {"id": 70111470,"title": "Die Hard","boxart":"http://cdn-0.nflximg.com/images/2891/DieHard150.jpg" } // ]; return movieLists // Complete this expression! } Run Show Answer Enter Fullscreen function(str) { preVerifierHook(); var fun = eval("(" + str + ")"), videos = fun(), got, expected = JSON.stringify([ {"id": 675465,"title": "Fracture","boxart":"http://cdn-0.nflximg.com/images/2891/Fracture150.jpg" }, {"id": 65432445,"title": "The Chamber","boxart":"http://cdn-0.nflximg.com/images/2891/TheChamber150.jpg" }, {"id": 654356453,"title": "Bad Boys","boxart":"http://cdn-0.nflximg.com/images/2891/BadBoys150.jpg" }, {"id": 70111470,"title": "Die Hard","boxart":"http://cdn-0.nflximg.com/images/2891/DieHard150.jpg" } ].sortBy(function(v) { return v.id })); if (str.indexOf('[0]') !== -1) { throw "You're not allowed to index into the array. You might be creating the object too early. Instead of using an indexer to get the boxart out of the array, try adding a call to map() and creating the object inside the projection function."; } videos = videos.sortBy(function(v) { return v.id }); got = JSON.stringify(videos); if (got !== expected) { showLessonErrorMessage(expected, got); } } function() { 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 }] } ] } ]; return movieLists. map(function(movieList) { return movieList.videos. map(function(video) { return video.boxarts. filter(function(boxart) { return boxart.width === 150 && boxart.height === 200; }). map(function(boxart) { return {id: video.id, title: video.title, boxart: boxart.url}; }); }). concatAll(); }). concatAll(); } Fantastic job! Now you've learned to use concatAll() alongside map() and filter() to query trees. Notice that map() and concatAll() are very commonly chained together. Let's create a small helper function to help us with this common pattern.

Exercise 13: Implement concatMap() Nearly every time we flatten a tree we chain map() and concatAll(). Sometimes, if we're dealing with a tree several levels deep, we'll repeat this combination many times in our code. To save on typing, let's create a concatMap function that's just a map operation, followed by a concatAll. Array.prototype.concatMap = function(projectionFunctionThatReturnsArray) { return this. map(function(item) { // ------------ INSERT CODE HERE! ---------------------------- // Apply the projection function to each item. The projection // function will return a new child array. This will create a // two-dimensional array. // ------------ INSERT CODE HERE! ---------------------------- }). // apply the concatAll function to flatten the two-dimensional array concatAll(); }; /* var spanishFrenchEnglishWords = [ ["cero","rien","zero"], ["uno","un","one"], ["dos","deux","two"] ]; // collect all the words for each number, in every language, in a single, flat list var allWords = [0,1,2]. concatMap(function(index) { return spanishFrenchEnglishWords[index]; }); return JSON.stringify(allWords) === '["cero","rien","zero","uno","un","one","dos","deux","two"]'; */ Run Show Answer Enter Fullscreen // Implement concatAll function(str) { preVerifierHook(); var fun = eval(str), spanishFrenchEnglishWords = [ ["cero","rien","zero"], ["uno","un","one"], ["dos","deux","two"] ], allWords = [0,1,2], result, expected = '["cero","rien","zero","uno","un","one","dos","deux","two"]'; var allWords = [0,1,2]. concatMap(function(index) { return spanishFrenchEnglishWords[index]; }); if (JSON.stringify(allWords) !== expected) { throw "Expected " + expected; } } Array.prototype.concatMap = function(projectionFunctionThatReturnsArray) { return this. map(function(item) { return projectionFunctionThatReturnsArray(item); }). // apply the concatAll function to flatten the two-dimensional array concatAll(); }; /* var spanishFrenchEnglishWords = [ ["cero","rien","zero"], ["uno","un","one"], ["dos","deux","two"] ]; // collect all the words for each number, in every language, in a single, flat list var allWords = [0,1,2]. concatMap(function(index) { return spanishFrenchEnglishWords[index]; }); return JSON.stringify(allWords) === '["cero","rien","zero","uno","un","one","dos","deux","two"]'; */ Now, instead of using map().concatAll() to flatten a tree, we can just use concatMap helper function.

Exercise 14: Use concatMap() to retrieve id, title, and 150x200 box art url for every video Let's repeat the exercise we just performed. However this time we'll simplify the code by replacing the map().concatAll() calls with concatMap(). function() { 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 }] } ] } ]; // Use one or more concatMap, map, and filter calls to create an array with the following items // [ // {"id": 675465, "title": "Fracture", "boxart": "http://cdn-0.nflximg.com/images/2891/Fracture150.jpg" }, // {"id": 65432445, "title": "The Chamber", "boxart": "http://cdn-0.nflximg.com/images/2891/TheChamber150.jpg" }, // {"id": 654356453, "title": "Bad Boys", "boxart": "http://cdn-0.nflximg.com/images/2891/BadBoys150.jpg" }, // {"id": 70111470, "title": "Die Hard", "boxart": "http://cdn-0.nflximg.com/images/2891/DieHard150.jpg" } // ]; return movieLists // Complete this expression! } Run Show Answer Enter Fullscreen function(str) { preVerifierHook(); var fun = eval("(" + str + ")"), videos = fun(), got, expected = JSON.stringify([ {"id": 675465,"title": "Fracture","boxart":"http://cdn-0.nflximg.com/images/2891/Fracture150.jpg" }, {"id": 65432445,"title": "The Chamber","boxart":"http://cdn-0.nflximg.com/images/2891/TheChamber150.jpg" }, {"id": 654356453,"title": "Bad Boys","boxart":"http://cdn-0.nflximg.com/images/2891/BadBoys150.jpg" }, {"id": 70111470,"title": "Die Hard","boxart":"http://cdn-0.nflximg.com/images/2891/DieHard150.jpg" } ].sortBy(function(v) { return v.id })); videos = videos.sortBy(function(v) { return v.id }); got = JSON.stringify(videos); if (got !== expected) { showLessonErrorMessage(expected, got); } } function() { 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 }] } ] } ]; return movieLists.concatMap(function(movieList) { return movieList.videos.concatMap(function(video) { return video.boxarts. filter(function(boxart) { return boxart.width === 150 && boxart.height === 200; }). map(function(boxart) { return {id: video.id, title: video.title, boxart: boxart.url}; }); }); }); } It's a very common pattern to see several nested concatMap operations, with the last operation being a map. You can think of this pattern as the functional version of a nested forEach.

Reducing Arrays Sometimes we need to perform an operation on more than one item in the array at the same time. For example, let's say we need to find the largest integer in an array. We can't use a filter() operation, because it only examines one item at a time. To find the largest integer we need to compare items in the array to each other. One approach could be to select an item in the array as the assumed largest number (perhaps the first item), and then compare that value to every other item in the array. Each time we come across a number that was larger than our assumed largest number, we'd replace it with the larger value, and continue the process until the entire array was traversed. If we replaced the specific size comparison with a closure, we could write a function that handled the array traversal process for us. At each step our function would apply the closure to the last value and the current value and use the result as the last value the next time. Finally we'd be left with only one value. This process is known as reducing because we reduce many values to a single value. Exercise 15: Use forEach to find the largest box art In this example we use forEach to find the largest box art. Each time we examine a new boxart we update a variable with the currently known maximumSize. If the boxart is smaller than the maximum size, we discard it. If it's larger, we keep track of it. Finally we're left with a single boxart which must necessarily be the largest. function() { var 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" }, { width: 425, height: 150, url: "http://cdn-0.nflximg.com/images/2891/Fracture425.jpg" } ], currentSize, maxSize = -1, largestBoxart; boxarts.forEach(function(boxart) { currentSize = boxart.width * boxart.height; if (currentSize > maxSize) { largestBoxart = boxart; maxSize = currentSize; } }); return largestBoxart; } Run Show Answer Enter Fullscreen // Find largest box art function(str){ preVerifierHook(); var fun = eval("(" + str + ")"), boxart = fun(), got = JSON.stringify(boxart), expected = JSON.stringify({ width: 425, height:150, url:"http://cdn-0.nflximg.com/images/2891/Fracture425.jpg" }); if (got !== expected) { showLessonErrorMessage(expected, got); } } function() { var 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" }, { width: 425, height: 150, url: "http://cdn-0.nflximg.com/images/2891/Fracture425.jpg" } ], currentSize, maxSize = -1, largestBoxart; boxarts.forEach(function(boxart) { currentSize = boxart.width * boxart.height; if (currentSize > maxSize) { largestBoxart = boxart; maxSize = currentSize; } }); return largestBoxart; } This process is a reduction because we're using the information we derived from the last computation to calculate the current value. However in the example above, we still have to specify the method of traversal. Wouldn't it be nice if we could just specify what operation we wanted to perform on the last and current value? Let's create a helper function to perform reductions on arrays.

Exercise 16: Implement reduce() Let's add a reduce() function to the Array type. Like map. Take note this is different from the reduce in ES5, which returns a value instead of an Array! // [1,2,3].reduce(function(accumulatedValue, currentValue) { return accumulatedValue + currentValue; }); === [6]; // [1,2,3].reduce(function(accumulatedValue, currentValue) { return accumulatedValue + currentValue; }, 10); === [16]; Array.prototype.reduce = function(combiner, initialValue) { var counter, accumulatedValue; // If the array is empty, do nothing if (this.length === 0) { return this; } else { // If the user didn't pass an initial value, use the first item. if (arguments.length === 1) { counter = 1; accumulatedValue = this[0]; } else if (arguments.length >= 2) { counter = 0; accumulatedValue = initialValue; } else { throw "Invalid arguments."; } // Loop through the array, feeding the current value and the result of // the previous computation back into the combiner function until // we've exhausted the entire array and are left with only one value. while(counter < this.length) { accumulatedValue = combiner(accumulatedValue, this[counter]) counter++; } return [accumulatedValue]; } }; Run Show Answer Enter Fullscreen // Implement reduce function(str) { preVerifierHook(); var fun = eval(str), numbers = [1,2,3], sum = numbers.reduce(function(acc,curr) { return acc + curr }), expected = JSON.stringify([6]), sum2 = numbers.reduce(function(acc,curr) { return acc + curr },10), expected2 = JSON.stringify([16]); if (JSON.stringify(sum) !== expected) { throw "Expected that [1,2,3].reduce(function(accumulated,current) { return accumulated + current; }) === [6]. Instead got " + JSON.stringify(sum); } if (JSON.stringify(sum2) !== expected2) { throw "Expected that [1,2,3].reduce(function(accumulated,current) { return accumulated + current; }, 10) === [16]. Instead got " + JSON.stringify(sum2); } } // [1,2,3].reduce(function(accumulatedValue, currentValue) { return accumulatedValue + currentValue; }); === [6]; // [1,2,3].reduce(function(accumulatedValue, currentValue) { return accumulatedValue + currentValue; }, 10); === [16]; Array.prototype.reduce = function(combiner, initialValue) { var counter, accumulatedValue; // If the array is empty, do nothing if (this.length === 0) { return this; } else { // If the user didn't pass an initial value, use the first item. if (arguments.length === 1) { counter = 1; accumulatedValue = this[0]; } else if (arguments.length >= 2) { counter = 0; accumulatedValue = initialValue; } else { throw "Invalid arguments."; } // Loop through the array, feeding the current value and the result of // the previous computation back into the combiner function until // we've exhausted the entire array and are left with only one function. while(counter < this.length) { accumulatedValue = combiner(accumulatedValue, this[counter]) counter++; } return [accumulatedValue]; } };

Exercise 17: Retrieve the largest rating. Let's use our new reduce function to isolate the largest value in an array of ratings. function() { var ratings = [2,3,1,4,5]; // You should return an array containing only the largest rating. Remember that reduce always // returns an array with one item. return ratings. reduce // Complete this expression } Run Show Answer Enter Fullscreen // Find largest rating function(str){ preVerifierHook(); var fun = eval("(" + str + ")"), boxarts = fun(), got = JSON.stringify(boxarts), expected = JSON.stringify([5]); if (got !== expected) { showLessonErrorMessage(expected, got); } } function() { var ratings = [2,3,1,4,5]; return ratings. reduce(function(acc, curr) { if(acc > curr) { return acc; } else { return curr; } }); } Nice work. Now let's try combining reduce() with our other functions to build more complex queries.

Exercise 18: Retrieve url of the largest boxart Let's try combining reduce() with map() to reduce multiple boxart objects to a single value: the url of the largest box art. function() { var 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" }, { width: 425, height: 150, url: "http://cdn-0.nflximg.com/images/2891/Fracture425.jpg" } ]; // You should return an array containing only the URL of the largest box art. Remember that reduce always // returns an array with one item. return boxarts. reduce // Complete this expression } Run Show Answer Enter Fullscreen // Find largest box art with reduce function(str){ preVerifierHook(); var fun = eval("(" + str + ")"), boxarts = fun(), got = JSON.stringify(boxarts), expected = JSON.stringify(["http://cdn-0.nflximg.com/images/2891/Fracture425.jpg"]); if (got !== expected) { showLessonErrorMessage(expected, got); } } function() { var 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" }, { width: 425, height: 150, url: "http://cdn-0.nflximg.com/images/2891/Fracture425.jpg" } ]; return boxarts. reduce(function(acc,curr) { if (acc.width * acc.height > curr.width * curr.height) { return acc; } else { return curr; } }). map(function(boxart) { return boxart.url; }); }

Exercise 19: Reducing with an initial value Sometimes when we reduce an array, we want the reduced value to be a different type than the items stored in the array. Let's say we have an array of videos and we want to reduce them to a single map where the key is the video id and the value is the video's title. function() { var videos = [ { "id": 65432445, "title": "The Chamber" }, { "id": 675465, "title": "Fracture" }, { "id": 70111470, "title": "Die Hard" }, { "id": 654356453, "title": "Bad Boys" } ]; // Expecting this output... // [ // { // "65432445": "The Chamber", // "675465": "Fracture", // "70111470": "Die Hard", // "654356453": "Bad Boys" // } // ] return videos. reduce(function(accumulatedMap, video) { var obj = {}; // ----- INSERT CODE TO ADD THE VIDEO TITLE TO THE ---- // ----- NEW MAP USING THE VIDEO ID AS THE KEY ---- // Object.assign() takes all of the enumerable properties from // the object listed in its second argument (obj) and assigns them // to the object listed in its first argument (accumulatedMap). return Object.assign(accumulatedMap, obj); }, // Use an empty map as the initial value instead of the first item in // the list. {}); } Run Show Answer Enter Fullscreen // Reducing with an initial value function(str){ preVerifierHook(); var fun = eval("(" + str + ")"), videoMap = fun()[0], expected = [ { "65432445": "The Chamber", "675465": "Fracture", "70111470": "Die Hard", "654356453": "Bad Boys" } ]; if (!(videoMap["65432445"] === "The Chamber" && videoMap["675465"] === "Fracture" && videoMap["70111470"] === "Die Hard" && videoMap["654356453"] === "Bad Boys")) { throw "Expected " + JSON.stringify(expected); } } function() { var videos = [ { "id": 65432445, "title": "The Chamber" }, { "id": 675465, "title": "Fracture" }, { "id": 70111470, "title": "Die Hard" }, { "id": 654356453, "title": "Bad Boys" } ]; // Expecting this output... // [ // { // "65432445": "The Chamber", // "675465": "Fracture", // "70111470": "Die Hard", // "654356453": "Bad Boys" // } // ] return videos. reduce(function(accumulatedMap, video) { var obj = {}; // ----- INSERT CODE TO ADD THE VIDEO TITLE TO THE ---- // ----- NEW MAP USING THE VIDEO ID AS THE KEY ---- obj[video.id] = video.title; // Object.assign() takes all of the enumerable properties from // the object listed in its second argument (obj) and assigns them // to the object listed in its first argument (accumulatedMap). return Object.assign(accumulatedMap, obj); }, // Use an empty map as the initial value instead of the first item in // the list. {}); } Nice work. Now let's try combining reduce() with our other functions to build more complex queries.

Exercise 20: Retrieve the id, title, and smallest box art url for every video. This is a variation of the problem we solved earlier, where we retrieved the url of the boxart with a width of 150px. This time we'll use reduce() instead of filter() to retrieve the smallest box art in the boxarts array. function() { var movieLists = [ { name: "New Releases", 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: 140, height:200, url:"http://cdn-0.nflximg.com/images/2891/BadBoys140.jpg" } ], "url": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 5.0, "bookmark": [{ id:432534, time:65876586 }] } ] }, { name: "Thrillers", videos: [ { "id": 65432445, "title": "The Chamber", "boxarts": [ { width: 130, height:200, url:"http://cdn-0.nflximg.com/images/2891/TheChamber130.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: 120, height:200, url:"http://cdn-0.nflximg.com/images/2891/Fracture120.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 }] } ] } ]; // Use one or more concatMap, map, and reduce calls to create an array with the following items (order matters) // [ // {"id": 675465,"title": "Fracture","boxart":"http://cdn-0.nflximg.com/images/2891/Fracture120.jpg" }, // {"id": 65432445,"title": "The Chamber","boxart":"http://cdn-0.nflximg.com/images/2891/TheChamber130.jpg" }, // {"id": 654356453,"title": "Bad Boys","boxart":"http://cdn-0.nflximg.com/images/2891/BadBoys140.jpg" }, // {"id": 70111470,"title": "Die Hard","boxart":"http://cdn-0.nflximg.com/images/2891/DieHard150.jpg" } // ]; return movieLists. concatMap(function(movieList) { }) } Run Show Answer Enter Fullscreen // Find the id, title, and smallest box art. function(str) { preVerifierHook(); var fun = eval("(" + str + ")"), videos = fun(), got, expected = JSON.stringify([ {"id": 675465,"title": "Fracture","boxart":"http://cdn-0.nflximg.com/images/2891/Fracture120.jpg" }, {"id": 65432445,"title": "The Chamber","boxart":"http://cdn-0.nflximg.com/images/2891/TheChamber130.jpg" }, {"id": 654356453,"title": "Bad Boys","boxart":"http://cdn-0.nflximg.com/images/2891/BadBoys140.jpg" }, {"id": 70111470,"title": "Die Hard","boxart":"http://cdn-0.nflximg.com/images/2891/DieHard150.jpg" } ].sortBy(function(v) { return v.id })); if (str.indexOf('[0]') !== -1){ throw "You're not allowed to index into the array. You might be creating the object too early. Instead of using an indexer to get the boxart out of the array, try adding a call to map() and creating the object inside the projection function."; } videos = videos.sortBy(function(v) { return v.id }); got = JSON.stringify(videos); if (got !== expected) { showLessonErrorMessage(expected, got); } } function() { var movieLists = [ { name: "New Releases", 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: 140, height:200, url:"http://cdn-0.nflximg.com/images/2891/BadBoys140.jpg" } ], "url": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 5.0, "bookmark": [{ id:432534, time:65876586 }] } ] }, { name: "Thrillers", videos: [ { "id": 65432445, "title": "The Chamber", "boxarts": [ { width: 130, height:200, url:"http://cdn-0.nflximg.com/images/2891/TheChamber130.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: 120, height:200, url:"http://cdn-0.nflximg.com/images/2891/Fracture120.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 }] } ] } ]; // Use one or more concatMap, map, and reduce calls to create an array with the following items (order matters) // [ // {"id": 675465,"title": "Fracture","boxart":"http://cdn-0.nflximg.com/images/2891/Fracture120.jpg" }, // {"id": 65432445,"title": "The Chamber","boxart":"http://cdn-0.nflximg.com/images/2891/TheChamber130.jpg" }, // {"id": 654356453,"title": "Bad Boys","boxart":"http://cdn-0.nflximg.com/images/2891/BadBoys140.jpg" }, // {"id": 70111470,"title": "Die Hard","boxart":"http://cdn-0.nflximg.com/images/2891/DieHard150.jpg" } // ]; return movieLists.concatMap(function(movieList) { return movieList.videos.concatMap(function(video) { return video.boxarts. reduce(function(acc,curr) { if (acc.width * acc.height < curr.width * curr.height) { return acc; } else { return curr; } }). map(function(boxart) { return {id: video.id, title: video.title, boxart: boxart.url}; }); }); }); }

Zipping Arrays Sometimes we need to combine two arrays by progressively taking an item from each and combining the pair. If you visualize a zipper, where each side is an array, and each tooth is an item, you'll have a good idea of how the zip operation works. Exercise 21: Combine videos and bookmarks by index Use a for loop to traverse the videos and bookmarks array at the same time. For each video and bookmark pair, create a {videoId, bookmarkId} pair and add it to the videoIdAndBookmarkIdPairs array. function() { var videos = [ { "id": 70111470, "title": "Die Hard", "boxart": "http://cdn-0.nflximg.com/images/2891/DieHard.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 4.0, }, { "id": 654356453, "title": "Bad Boys", "boxart": "http://cdn-0.nflximg.com/images/2891/BadBoys.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 5.0, }, { "id": 65432445, "title": "The Chamber", "boxart": "http://cdn-0.nflximg.com/images/2891/TheChamber.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 4.0, }, { "id": 675465, "title": "Fracture", "boxart": "http://cdn-0.nflximg.com/images/2891/Fracture.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 5.0, } ], bookmarks = [ {id: 470, time: 23432}, {id: 453, time: 234324}, {id: 445, time: 987834} ], counter, videoIdAndBookmarkIdPairs = []; for(counter = 0; counter < Math.min(videos.length, bookmarks.length); counter++) { // Insert code here to create a {videoId, bookmarkId} pair and add it to the videoIdAndBookmarkIdPairs array. } return videoIdAndBookmarkIdPairs; } Run Show Answer Enter Fullscreen // Zip imperatively function(str) { preVerifierHook(); var fun = eval("(" + str + ")"), pairs = fun(), got, expected = '[{"videoId":65432445,"bookmarkId":445},{"videoId":70111470,"bookmarkId":470},{"videoId":654356453,"bookmarkId":453}]'; pairs = pairs.sortBy(function(v) { return v.videoId }); got = JSON.stringify(pairs); if (got !== expected) { showLessonErrorMessage(expected, got); } } function() { var videos = [ { "id": 70111470, "title": "Die Hard", "boxart": "http://cdn-0.nflximg.com/images/2891/DieHard.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 4.0, }, { "id": 654356453, "title": "Bad Boys", "boxart": "http://cdn-0.nflximg.com/images/2891/BadBoys.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 5.0, }, { "id": 65432445, "title": "The Chamber", "boxart": "http://cdn-0.nflximg.com/images/2891/TheChamber.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 4.0, }, { "id": 675465, "title": "Fracture", "boxart": "http://cdn-0.nflximg.com/images/2891/Fracture.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 5.0, } ], bookmarks = [ {id: 470, time: 23432}, {id: 453, time: 234324}, {id: 445, time: 987834} ], counter, videoIdAndBookmarkIdPairs = []; for(counter = 0; counter < Math.min(videos.length, bookmarks.length); counter++) { videoIdAndBookmarkIdPairs.push({videoId: videos[counter].id, bookmarkId: bookmarks[counter].id}); } return videoIdAndBookmarkIdPairs; }

Exercise 22: Implement zip Let's add a static zip() function to the Array type. The zip function accepts a combiner function, traverses each array at the same time, and calls the combiner function on the current item on the left-hand-side and right-hand-side. The zip function requires an item from each array in order to call the combiner function, therefore the array returned by zip will only be as large as the smallest input array. // JSON.stringify(Array.zip([1,2,3],[4,5,6], function(left, right) { return left + right })) === '[5,7,9]' Array.zip = function(left, right, combinerFunction) { var counter, results = []; for(counter = 0; counter < Math.min(left.length, right.length); counter++) { // Add code here to apply the combinerFunction to the left and right-hand items in the respective arrays } return results; }; Run Show Answer Enter Fullscreen // Implement zip function(str) { preVerifierHook(); var fun = eval(str), left = [1,2,3], right = [4,5,6], sum = Array.zip(left, right, function(left, right){ return left + right; }), expected = '[5,7,9]'; if (JSON.stringify(sum) !== expected) { showLessonErrorMessage(expected, JSON.stringify(sum)); } } // JSON.stringify(Array.zip([1,2,3],[4,5,6], function(left, right) { return left + right })) === '[5,7,9]' Array.zip = function(left, right, combinerFunction) { var counter, results = []; for(counter = 0; counter < Math.min(left.length, right.length); counter++) { results.push(combinerFunction(left[counter],right[counter])); } return results; };

Exercise 23: Combine videos and bookmarks by index Let's repeat exercise 21, but this time lets use your new zip() function. For each video and bookmark pair, create a {videoId, bookmarkId} pair. function() { var videos = [ { "id": 70111470, "title": "Die Hard", "boxart": "http://cdn-0.nflximg.com/images/2891/DieHard.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 4.0, }, { "id": 654356453, "title": "Bad Boys", "boxart": "http://cdn-0.nflximg.com/images/2891/BadBoys.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 5.0, }, { "id": 65432445, "title": "The Chamber", "boxart": "http://cdn-0.nflximg.com/images/2891/TheChamber.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 4.0, }, { "id": 675465, "title": "Fracture", "boxart": "http://cdn-0.nflximg.com/images/2891/Fracture.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 5.0, } ], bookmarks = [ {id: 470, time: 23432}, {id: 453, time: 234324}, {id: 445, time: 987834} ]; return Array. zip( //... finish this expression } Run Show Answer Enter Fullscreen // Combine videos and bookmarks function(str) { preVerifierHook(); var fun = eval("(" + str + ")"), pairs = fun(), got, expected = '[{"videoId":65432445,"bookmarkId":445},{"videoId":70111470,"bookmarkId":470},{"videoId":654356453,"bookmarkId":453}]'; pairs = pairs.sortBy(function(v) { return v.videoId }); got = JSON.stringify(pairs); if (got !== expected) { showLessonErrorMessage(expected, got); } } function() { var videos = [ { "id": 70111470, "title": "Die Hard", "boxart": "http://cdn-0.nflximg.com/images/2891/DieHard.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 4.0, }, { "id": 654356453, "title": "Bad Boys", "boxart": "http://cdn-0.nflximg.com/images/2891/BadBoys.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 5.0, }, { "id": 65432445, "title": "The Chamber", "boxart": "http://cdn-0.nflximg.com/images/2891/TheChamber.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 4.0, }, { "id": 675465, "title": "Fracture", "boxart": "http://cdn-0.nflximg.com/images/2891/Fracture.jpg", "uri": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 5.0, } ], bookmarks = [ {id: 470, time: 23432}, {id: 453, time: 234324}, {id: 445, time: 987834} ]; return Array. zip( videos, bookmarks, function(video, bookmark) { return {videoId: video.id, bookmarkId: bookmark.id}; }); }

Exercise 24: Retrieve each video's id, title, middle interesting moment time, and smallest box art url. This is a variation of the problem we solved earlier. This time each video has an interesting moments collection, each representing a time during which a screenshot is interesting or representative of the title as a whole. Notice that both the boxarts and interestingMoments arrays are located at the same depth in the tree. Retrieve the time of the middle interesting moment and the smallest box art url simultaneously with zip(). Return an {id, title, time, url} object for each video. function() { var movieLists = [ { name: "New Releases", 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, "interestingMoments": [ { type: "End", time:213432 }, { type: "Start", time: 64534 }, { type: "Middle", time: 323133} ] }, { "id": 654356453, "title": "Bad Boys", "boxarts": [ { width: 200, height:200, url:"http://cdn-0.nflximg.com/images/2891/BadBoys200.jpg" }, { width: 140, height:200, url:"http://cdn-0.nflximg.com/images/2891/BadBoys140.jpg" } ], "url": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 5.0, "interestingMoments": [ { type: "End", time:54654754 }, { type: "Start", time: 43524243 }, { type: "Middle", time: 6575665} ] } ] }, { name: "Instant Queue", videos: [ { "id": 65432445, "title": "The Chamber", "boxarts": [ { width: 130, height:200, url:"http://cdn-0.nflximg.com/images/2891/TheChamber130.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, "interestingMoments": [ { type: "End", time:132423 }, { type: "Start", time: 54637425 }, { type: "Middle", time: 3452343} ] }, { "id": 675465, "title": "Fracture", "boxarts": [ { width: 200, height:200, url:"http://cdn-0.nflximg.com/images/2891/Fracture200.jpg" }, { width: 120, height:200, url:"http://cdn-0.nflximg.com/images/2891/Fracture120.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, "interestingMoments": [ { type: "End", time:45632456 }, { type: "Start", time: 234534 }, { type: "Middle", time: 3453434} ] } ] } ]; //------------ COMPLETE THIS EXPRESSION -------------- return movieLists. concatMap(function(movieList) { }); } Run Show Answer Enter Fullscreen // Find id, title, smallest box art, and bookmark id function(str) { preVerifierHook(); var fun = eval("(" + str + ")"), videos = fun(), got, expected = '[{"id":675465,"title":"Fracture","time":3453434,"url":"http://cdn-0.nflximg.com/images/2891/Fracture120.jpg"},{"id":65432445,"title":"The Chamber","time":3452343,"url":"http://cdn-0.nflximg.com/images/2891/TheChamber130.jpg"},{"id":70111470,"title":"Die Hard","time":323133,"url":"http://cdn-0.nflximg.com/images/2891/DieHard150.jpg"},{"id":654356453,"title":"Bad Boys","time":6575665,"url":"http://cdn-0.nflximg.com/images/2891/BadBoys140.jpg"}]'; videos = videos.sortBy(function(v) { return v.id }); got = JSON.stringify(videos); if (got !== expected) { showLessonErrorMessage(expected, got); } } function() { var movieLists = [ { name: "New Releases", 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, "interestingMoments": [ { type: "End", time:213432 }, { type: "Start", time: 64534 }, { type: "Middle", time: 323133} ] }, { "id": 654356453, "title": "Bad Boys", "boxarts": [ { width: 200, height:200, url:"http://cdn-0.nflximg.com/images/2891/BadBoys200.jpg" }, { width: 140, height:200, url:"http://cdn-0.nflximg.com/images/2891/BadBoys140.jpg" } ], "url": "http://api.netflix.com/catalog/titles/movies/70111470", "rating": 5.0, "interestingMoments": [ { type: "End", time:54654754 }, { type: "Start", time: 43524243 }, { type: "Middle", time: 6575665} ] } ] }, { name: "Instant Queue", videos: [ { "id": 65432445, "title": "The Chamber", "boxarts": [ { width: 130, height:200, url:"http://cdn-0.nflximg.com/images/2891/TheChamber130.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, "interestingMoments": [ { type: "End", time:132423 }, { type: "Start", time: 54637425 }, { type: "Middle", time: 3452343} ] }, { "id": 675465, "title": "Fracture", "boxarts": [ { width: 200, height:200, url:"http://cdn-0.nflximg.com/images/2891/Fracture200.jpg" }, { width: 120, height:200, url:"http://cdn-0.nflximg.com/images/2891/Fracture120.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, "interestingMoments": [ { type: "End", time:45632456 }, { type: "Start", time: 234534 }, { type: "Middle", time: 3453434} ] } ] } ]; //------------ COMPLETE THIS EXPRESSION -------------- return movieLists.concatMap(function(movieList) { return movieList.videos.concatMap(function(video) { return Array.zip( video.boxarts.reduce(function(acc,curr) { if (acc.width * acc.height < curr.width * curr.height) { return acc; } else { return curr; } }), video.interestingMoments.filter(function(interestingMoment) { return interestingMoment.type === "Middle"; }), function(boxart, interestingMoment) { return {id: video.id, title: video.title, time: interestingMoment.time, url: boxart.url}; }); }); }); }

Powerful Queries Now that we've learned the five operators let's flex our muscles and write some powerful queries. Exercise 25: Converting from Arrays to Trees When information is organized in a tree like a JSON expression, relationships point from parent to child. In relational systems like databases, relationships point from children to their parents. Both ways of organizing information are equivalent, and depending on the circumstances, we might get data organized in one way or another. It may surprise you to learn that you can use the 5 query functions you already know to easily convert between these representations. In other words, not only can you query arrays from trees, you can query trees from arrays. We have 2 arrays each containing lists, and videos respectively. Each video has a listId field indicating its parent list. We want to build an array of list objects, each with a name and a videos array. The videos array will contain the video's id and title. In other words we want to build the following structure: [ { "name": "New Releases", "videos": [ { "id": 65432445, "title": "The Chamber" }, { "id": 675465, "title": "Fracture" } ] }, { "name": "Thrillers", "videos": [ { "id": 70111470, "title": "Die Hard" }, { "id": 654356453, "title": "Bad Boys" } ] } ] Note: please make sure when creating objects (both lists and videos) that you add properties in the same order as above. This doesn't impact the correctness of your code, but the verifier expects properties to be created in this order. function() { var lists = [ { "id": 5434364, "name": "New Releases" }, { "id": 65456475, "name": "Thrillers" } ], videos = [ { "listId": 5434364, "id": 65432445, "title": "The Chamber" }, { "listId": 5434364, "id": 675465, "title": "Fracture" }, { "listId": 65456475, "id": 70111470, "title": "Die Hard" }, { "listId": 65456475, "id": 654356453, "title": "Bad Boys" } ]; return lists. // complete this expression } Run Show Answer Enter Fullscreen // Combine videos and bookmarks function(str) { preVerifierHook(); var fun = eval("(" + str + ")"), pairs = fun(), got, expected = '[{"name":"New Releases","videos":[{"id":65432445,"title":"The Chamber"},{"id":675465,"title":"Fracture"}]},{"name":"Thrillers","videos":[{"id":70111470,"title":"Die Hard"},{"id":654356453,"title":"Bad Boys"}]}]'; got = JSON.stringify(pairs); if (got !== expected) { showLessonErrorMessage(expected, got); } } function() { var lists = [ { "id": 5434364, "name": "New Releases" }, { "id": 65456475, name: "Thrillers" } ], videos = [ { "listId": 5434364, "id": 65432445, "title": "The Chamber" }, { "listId": 5434364, "id": 675465, "title": "Fracture" }, { "listId": 65456475, "id": 70111470, "title": "Die Hard" }, { "listId": 65456475, "id": 654356453, "title": "Bad Boys" } ]; return lists.map(function(list) { return { name: list.name, videos: videos. filter(function(video) { return video.listId === list.id; }). map(function(video) { return {id: video.id, title: video.title}; }) }; }); } Looks like you figured out that you can use map and filter to join two different arrays by a key. Now let's try a more complex example...

Exercise 26: Converting from Arrays to Deeper Trees Let's try creating a deeper tree structure. This time we have 4 separate arrays each containing lists, videos, boxarts, and bookmarks respectively. Each object has a parent id, indicating its parent. We want to build an array of list objects, each with a name and a videos array. The videos array will contain the video's id, title, bookmark time, and smallest boxart url. In other words we want to build the following structure: [ { "name": "New Releases", "videos": [ { "id": 65432445, "title": "The Chamber", "time": 32432, "boxart": "http://cdn-0.nflximg.com/images/2891/TheChamber130.jpg" }, { "id": 675465, "title": "Fracture", "time": 3534543, "boxart": "http://cdn-0.nflximg.com/images/2891/Fracture120.jpg" } ] }, { "name": "Thrillers", "videos": [ { "id": 70111470, "title": "Die Hard", "time": 645243, "boxart": "http://cdn-0.nflximg.com/images/2891/DieHard150.jpg" }, { "id": 654356453, "title": "Bad Boys", "time": 984934, "boxart": "http://cdn-0.nflximg.com/images/2891/BadBoys140.jpg" } ] } ] Note: please make sure when creating objects (both lists and videos) that you add properties in the same order as above. This doesn't impact the correctness of your code, but the verifier expects properties to be created in this order. function() { var lists = [ { "id": 5434364, "name": "New Releases" }, { "id": 65456475, name: "Thrillers" } ], videos = [ { "listId": 5434364, "id": 65432445, "title": "The Chamber" }, { "listId": 5434364, "id": 675465, "title": "Fracture" }, { "listId": 65456475, "id": 70111470, "title": "Die Hard" }, { "listId": 65456475, "id": 654356453, "title": "Bad Boys" } ], boxarts = [ { videoId: 65432445, width: 130, height:200, url:"http://cdn-0.nflximg.com/images/2891/TheChamber130.jpg" }, { videoId: 65432445, width: 200, height:200, url:"http://cdn-0.nflximg.com/images/2891/TheChamber200.jpg" }, { videoId: 675465, width: 200, height:200, url:"http://cdn-0.nflximg.com/images/2891/Fracture200.jpg" }, { videoId: 675465, width: 120, height:200, url:"http://cdn-0.nflximg.com/images/2891/Fracture120.jpg" }, { videoId: 675465, width: 300, height:200, url:"http://cdn-0.nflximg.com/images/2891/Fracture300.jpg" }, { videoId: 70111470, width: 150, height:200, url:"http://cdn-0.nflximg.com/images/2891/DieHard150.jpg" }, { videoId: 70111470, width: 200, height:200, url:"http://cdn-0.nflximg.com/images/2891/DieHard200.jpg" }, { videoId: 654356453, width: 200, height:200, url:"http://cdn-0.nflximg.com/images/2891/BadBoys200.jpg" }, { videoId: 654356453, width: 140, height:200, url:"http://cdn-0.nflximg.com/images/2891/BadBoys140.jpg" } ], bookmarks = [ { videoId: 65432445, time: 32432 }, { videoId: 675465, time: 3534543 }, { videoId: 70111470, time: 645243 }, { videoId: 654356453, time: 984934 } ]; return lists. // complete this expression } Run Show Answer Enter Fullscreen // Combine videos and bookmarks function(str) { preVerifierHook(); var fun = eval("(" + str + ")"), pairs = fun(), got, expected = '[{"name":"New Releases","videos":[{"id":65432445,"title":"The Chamber","time":32432,"boxart":"http://cdn-0.nflximg.com/images/2891/TheChamber130.jpg"},{"id":675465,"title":"Fracture","time":3534543,"boxart":"http://cdn-0.nflximg.com/images/2891/Fracture120.jpg"}]},{"name":"Thrillers","videos":[{"id":70111470,"title":"Die Hard","time":645243,"boxart":"http://cdn-0.nflximg.com/images/2891/DieHard150.jpg"},{"id":654356453,"title":"Bad Boys","time":984934,"boxart":"http://cdn-0.nflximg.com/images/2891/BadBoys140.jpg"}]}]'; got = JSON.stringify(pairs); if (got !== expected) { showLessonErrorMessage(expected, got); } } function() { var lists = [ { "id": 5434364, "name": "New Releases" }, { "id": 65456475, name: "Thrillers" } ], videos = [ { "listId": 5434364, "id": 65432445, "title": "The Chamber" }, { "listId": 5434364, "id": 675465, "title": "Fracture" }, { "listId": 65456475, "id": 70111470, "title": "Die Hard" }, { "listId": 65456475, "id": 654356453, "title": "Bad Boys" } ], boxarts = [ { videoId: 65432445, width: 130, height:200, url:"http://cdn-0.nflximg.com/images/2891/TheChamber130.jpg" }, { videoId: 65432445, width: 200, height:200, url:"http://cdn-0.nflximg.com/images/2891/TheChamber200.jpg" }, { videoId: 675465, width: 200, height:200, url:"http://cdn-0.nflximg.com/images/2891/Fracture200.jpg" }, { videoId: 675465, width: 120, height:200, url:"http://cdn-0.nflximg.com/images/2891/Fracture120.jpg" }, { videoId: 675465, width: 300, height:200, url:"http://cdn-0.nflximg.com/images/2891/Fracture300.jpg" }, { videoId: 70111470, width: 150, height:200, url:"http://cdn-0.nflximg.com/images/2891/DieHard150.jpg" }, { videoId: 70111470, width: 200, height:200, url:"http://cdn-0.nflximg.com/images/2891/DieHard200.jpg" }, { videoId: 654356453, width: 200, height:200, url:"http://cdn-0.nflximg.com/images/2891/BadBoys200.jpg" }, { videoId: 654356453, width: 140, height:200, url:"http://cdn-0.nflximg.com/images/2891/BadBoys140.jpg" } ], bookmarks = [ { videoId: 65432445, time: 32432 }, { videoId: 675465, time: 3534543 }, { videoId: 70111470, time: 645243 }, { videoId: 654356453, time: 984934 } ]; return lists.map(function(list) { return { name: list.name, videos: videos. filter(function(video) { return video.listId === list.id; }). concatMap(function(video) { return Array.zip( bookmarks.filter(function(bookmark) { return bookmark.videoId === video.id; }), boxarts.filter(function(boxart) { return boxart.videoId === video.id; }). reduce(function(acc,curr) { return acc.width * acc.height < curr.width * curr.height ? acc : curr; }), function(bookmark, boxart) { return { id: video.id, title: video.title, time: bookmark.time, boxart: boxart.url }; }); }) }; }); } Wow! That's a large query, but the code is still small relative to the amount of work it's doing. If we rewrote this query with a series of loops our code would be much less self-describing. Loops don't give the reader any information about the kind of operation being performed. Every time you see a loop, you need to carefully read through the code to find out what's being done. Is it a projection? A filter? A reduction? Why use loops for querying data when we've demonstrated that the 5 functions can be used to create virtually any output we want?

Exercise 27: Stock Ticker Let's try an easier question. Let's say we have a collection of all of the prices for NASDAQ stocks over time. Every time the price of a stock changes on the NASDAQ ticker an entry is added to this collection. Let's say that ten days ago you bought shares in Microsoft, and now you want to print all of the MSFT share prices since then. Filter the collection for MSFT trades starting from ten days ago and print each price record (including the time stamp) using the print() function. Note: this is not a trick question. It's as easy as it seems. // The pricesNASDAQ collection looks something like this... var pricesNASDAQ = [ // ... from the NASDAQ's opening day {name: "ANGI", price: 31.22, timeStamp: new Date(2011,11,15) }, {name: "MSFT", price: 32.32, timeStamp: new Date(2011,11,15) }, {name: "GOOG", price: 150.43, timeStamp: new Date(2011,11,15)}, {name: "ANGI", price: 28.44, timeStamp: new Date(2011,11,16)}, {name: "GOOG", price: 199.33, timeStamp: new Date(2011,11,16)}, // ...and up to the present. ]; function(pricesNASDAQ, printRecord) { var microsoftPrices, now = new Date(), tenDaysAgo = new Date( now.getFullYear(), now.getMonth(), now.getDate() - 10); // use filter() to filter the trades for MSFT prices recorded any time after 10 days ago microsoftPrices = pricesNASDAQ. filter(function(priceRecord) { // finish this expression // Print the trades to the output console microsoftPrices. forEach(function(priceRecord) { printRecord(priceRecord); }); } Run Show Answer Enter Fullscreen Console // Combine videos and bookmarks function(str, lesson) { preVerifierHook(); var output = $(".output", lesson)[0], fun = eval("(" + str + ")"), stockSymbols = ["MSFT", "GOOG","NFLX","OSTK"], input = [{name: "MSFT", price: 32.32, timeStamp: new Date() }, {name: "GOOG", price: 150.43, timeStamp: new Date(2011,11,15)}], expected = [input[0]], items = [], counter = 0; confirmPrint = function(item) { items.push(item); }, print = function(item) { output.innerHTML += "MSFT " + item.price + " at " + item.timeStamp.toString() + ""; output.scrollTop = output.scrollHeight; counter++; if (counter % 100 === 0) { output.innerHTML = ""; } }, stocks = Rx.Observable. interval(250). map(function() { var symbol = stockSymbols[Math.floor(Math.random() * stockSymbols.length)]; return {name: symbol, price: 30 + ((Math.floor(Math.random() * 100))/200), timeStamp: new Date()}; }); fun(input, confirmPrint); if (JSON.stringify(items) !== JSON.stringify(expected)) { throw "Got " + JSON.stringify(items, null, 4) + ", expected " + JSON.stringify(expected, null, 4); } fun(stocks, print); } function(pricesNASDAQ, printRecord) { var microsoftPrices, now = new Date(), tenDaysAgo = new Date( now.getFullYear(), now.getMonth(), now.getDate() - 10); // use filter() to filter the trades for MSFT prices recorded any time after 10 days ago microsoftPrices = pricesNASDAQ. filter(function(priceRecord) { return priceRecord.name === 'MSFT' && priceRecord.timeStamp > tenDaysAgo; }); // Print the trades to the output console microsoftPrices. forEach(function(priceRecord) { printRecord(priceRecord); }); } Notice that the console is changing over time. Now look at the time stamps on the stock prices. We're displaying stock prices sampled after we ran our program! How could our array have contained stock price records from the future? Did we accidentally rip a hole in the space-time continuum? The solution to the riddle is that pricesNASDAQ is not an array. Unlike an array, which can only store a snapshot of stock prices, this new type can react to changes and update over time. In the next section I'll reveal the inner workings of this magical type. You'll learn how you can use it to model everything from mouse events to asynchronous JSON requests. Finally I'll show you how you can query this type using the 5 query functions you already know. It's about time we gave this magical type a name... Working with Observables Microsoft's open-source Reactive Extensions library introduces a new collection type to Javascript: Observable. An Observable is a lot like an Event. Like an Event, an Observable is a sequence of values that a data producer pushes to the consumer. However unlike an Event, an Observable can signal to a listener that it has completed, and will send no more data. Observables can send data to consumers asynchronously. Unlike Array, there's no Javascript literal syntax for creating Observable sequences. However we can build a helper method that visually describes the contents of sequences as well as the times between each item's arrival. The seq function creates an Observable from an array of items, and adds a delay for every empty item encountered. Every ,,, adds up to a second. // An array of numbers 1,2,3 var numbers123Array = [1,2,3]; // A sequence that returns 1, and then after 4 seconds returns 2, // and then after another second returns 3, and then completes. var numbers123Observable = seq([1,,,,,,,,,,,,2,,,3]); // Like Arrays, Observables can contain any object - even Arrays. var observableOfArrays = seq([ [1,2,3],,,,,,[4,5,6],,,,,,,,,,,[1,2] ]); Observables are a sequence of values, delivered one after the other. Therefore it's possible that an Observable can go on sending data to a listener forever just like a mouse move event. To create a sequence that doesn't complete, you can add a trailing ,,, to the end of the items passed to seq(). // The trailing ,,, ensures that the sequence will not complete. var mouseMovesObservable = seq([ {x: 23, y: 55},,,,,,,{x: 44, y: 99},,,{x:55,y:99},,,{x: 54, y:543},,, ]); // No trailing ,,, indicates that sequence will complete. var numbers123Observable = seq([1,,,2,,,3]); Querying Arrays only gives us a snapshot. By contrast, querying Observables allows us to create data sets that react and update as the system changes over time. This enables a very powerful type of programming known as reactive programming. Let's start off by contrasting Observable with Events...

Exercise 28: Subscribing to an event You're probably used to thinking about events as a list of handlers stored in an object. In this example, we subscribe to a button click event and then unsubscribe the first time the button is clicked. function(button) { // the button click handler var handler = function(ev) { // Unsubscribe from the button event. button.removeEventListener("click", handler); alert("Button was clicked. Unsubscribing from event."); }; // add the button click handler button.addEventListener("click", handler); } Run Click this button after running the program // Combine videos and bookmarks 2 function(str, lesson) { preVerifierHook(); var fun = eval("(" + str + ")"), button = $('.button', lesson)[0]; fun(button); } Ask yourself this question: How is subscribing to an event different than traversing an array? Both operations involve sending a listener a sequence of items by repeatedly invoking a function. So why can't we traverse Arrays and Events the same way?

Exercise 29: Traversing an Event Subscribing to an Event and traversing an Array are fundamentally the same operation. The only difference is that Array traversal is synchronous and completes, and Event traversal is asynchronous and never completes . If we convert a button click Event to an Observable object, we can use do() to traverse the Event. function(button) { var buttonClicks = Observable.fromEvent(button, "click"); var subscription = buttonClicks. do(function(clickEvent) { alert("Button was clicked. Stopping Traversal."); // Stop traversing the button clicks subscription.unsubscribe(); }). subscribe(); } Run Click this button after running the program // Combine videos and bookmarks 2 function(str, lesson) { preVerifierHook(); var fun = eval("(" + str + ")"), button = $('.button', lesson)[0]; fun(button); } Notice that Observable's forEach() function returns a Subscription object. Disposing of a Subscription object unsubscribes from the event and prevents memory leaks. Disposing of a subscription is the asynchronous equivalent of stopping half-way through a counting for loop. Disposing of a Subscription object is basically the same as calling removeEventListener(). On the surface, these two approaches to Event handling don't seem to be very different. Under the circumstances, why should we bother converting Events to Observables? The reason is that if we convert Events to Observable Objects, we can use powerful functions to transform them. In the next exercise we'll learn how we can use one such function to avoid dealing with Subscriptions in many cases...

Exercise 30: Completing Sequences with take() Have you ever wished that you could listen for the next occurrence of an event and then immediately unsubscribe? For example, developers will often attach an event handler to window.onload, expecting that their event handler will only be called once. window.addEventListener( "load", function() // do some work here, but expect this function will only be called once. }) In instances such as this, it's good practice to unsubscribe from the event after it's fired. Failing to unsubscribe is a memory leak. Depending on the circumstances, memory leaks can seriously destablize your application and can be very difficult to track down. Unfortunately unsubscribing from an event after one occurrence can be cumbersome: var handler = function() { // do some work here, then unsubscribe from the event window.removeEventListener("load", handler) }; window.addEventListener("load", handler); Wouldn't it be nice if there was an easier way to code this? That's why Observable has a take() function. The take() function works like this... seq([1,,,2,,,3,,,4,,,5,,,6,,,]).take(3) === seq([1,,,2,,,3]); An Observable based on an Event will never complete on its own. The take() function creates a new sequence that completes after a discrete number of items arrive. This is important, because unlike an Event, when an Observable sequence completes it unsubscribes all of its listeners. That means that if we use take() to complete our Event sequence, we don't need to unsubscribe! Let's repeat the previous exercise, in which we listened for a single button click and then unsubscribed. This time let's use the take() function to complete the sequence after the button is clicked. function(button) { var buttonClicks = Observable.fromEvent(button, "click"); // Use take() to listen for only one button click // and unsubscribe. buttonClicks. // Insert take() call here forEach(function(clickEvent) { alert("Button was clicked once. Stopping Traversal."); }); } Run Show Answer Enter Fullscreen Click this button after pressing the run button // Combine videos and bookmarks 2 function(str, lesson) { preVerifierHook(); var fun = eval("(" + str + ")"), button = $('.button', lesson)[0]; fun(button); } function(button) { var buttonClicks = Observable.fromEvent(button, "click"); // Use take() to listen for only one button click // and unsubscribe. buttonClicks. take(1). forEach(function() { alert("Button was clicked once. Stopping Traversal."); }); } The take() function is great way of listening for a discrete number of events and then unsubscribing, but Observable has an even more flexible function that we can use to complete sequences...

Exercise 31: Completing sequences with takeUntil() Have you ever wanted to unsubscribe from one Event when another Event fires? Observable's takeUntil() function is a convenient way of completing a sequence when another Event occurs. Here's how takeUntil() works: var numbersUntilStopButtonPressed = seq( [ 1,,,2,,,3,,,4,,,5,,,6,,,7,,,8,,,9,,, ]). takeUntil(seq([ ,,, {eventType: "click"},,, ]) ) === seq([ 1,,,2 ]) Earlier we (unknowningly) built a dynamic Microsoft price stock ticker using Observable. The problem with that stock ticker was that it kept going on forever. If left unchecked, all the entries in the log could use up all of the memory on the page. In the exercise below, filter the Observable sequence of NASDAQ prices for MSFT stock prices, use the fromEvent() function to create an Observable . function(pricesNASDAQ, printRecord, stopButton) { var stopButtonClicks = // ----- To finish this expression, use Observable.fromEvent to convert the "click" event on the stop button to an Observable microsoftPrices = pricesNASDAQ. filter(function(priceRecord) { return priceRecord.name === "MSFT"; }). // ----- To finish this expression, use takeUntil to complete the sequence when stopButtonClicks fires. microsoftPrices. forEach(function(priceRecord) { printRecord(priceRecord); }); } Run Show Answer Enter Fullscreen Stop <-Press this button to complete the sequence of microsoft stock prices. Console // Combine videos and bookmarks 2 function(str, lesson) { preVerifierHook(); var fun = eval("(" + str + ")"), output = $(".output", lesson)[0], stopButton = $('.stop', lesson)[0], stockSymbols = ["MSFT", "GOOG","NFLX","OSTK"], input = [{name: "MSFT", price: 32.32, timeStamp: new Date() }, {name: "GOOG", price: 150.43, timeStamp: new Date(2011,11,15)}], expected = [input[0]], items = [], counter = 0, print = function(item) { output.innerHTML += "MSFT " + item.price + " at " + item.timeStamp.toString() + ""; output.scrollTop = output.scrollHeight; counter++; if (counter % 100 === 0) { output.innerHTML = ""; } }, stocks = Rx.Observable. interval(250). map(function() { var symbol = stockSymbols[Math.floor(Math.random() * stockSymbols.length)]; return {name: symbol, price: 30 + ((Math.floor(Math.random() * 100))/200), timeStamp: new Date()}; }); fun(stocks, print, stopButton); } function(pricesNASDAQ, printRecord, stopButton) { var stopButtonClicks = Observable.fromEvent(stopButton,"click"), microsoftPrices = pricesNASDAQ. filter(function(priceRecord) { return priceRecord.name === "MSFT"; }). takeUntil(stopButtonClicks); microsoftPrices. forEach(function(priceRecord) { printRecord(priceRecord); }); } We've learned that Observable sequences are much more powerful than raw events, because they can complete. The take() and takeUntil() functions are powerful enough to ensure that we never have to unsubscribe from another event again! This reduces the risk of memory leaks and makes our code more readable. Here's what we learned in this section: We can traverse Observables using forEach().

We can use fromEvent() to convert Events into Observables that never complete.

We can apply take() and takeUntil() to an Observable to create a new sequence which does complete. In the next section we'll learn how to combine events to create more complex events. You'll be suprised how easily you can solve complex, asynchronous problems! Querying Observables What's the difference between these two tasks? Creating a flat list of movies with a rating of 5.0 from a bunch of movie lists.

Creating a sequence of all the mouse drag events from the mouseDown, mouseMove, and mouseUp events. You might think of them as different, and might code them very differently, but these tasks are fundamentally the same. Both of these tasks are queries, and can be solved using the functions you've learned in these exercises. The difference between traversing an Array and traversing an Observable is the direction in which the data moves. When traversing an Array, the client pulls data from the data source, blocking until it gets a result. When traversing Observables, the data source pushes data at the client whenever it arrives. It turns out that the direction in which data moves is orthogonal to querying that data. In other words, when we're querying data it doesn't matter whether 