I think I am in love

It's Friday again, 8thLU time! If you are reading this for the first time, 8th Light University is the time set aside on Fridays for us to have talks, work on side projects, to keep mastering our craft! This time we had a bit of a teaser from Skim on his upcoming talk about ECMA-5 vs ECMA-6. Then he gave us this set of exercises by Jafar Husain. It is titled "Functional Programming in Javascript", sounds very enticing, doesn't it? What does 'functional' actually mean? I had a vague idea before, but I can't say I was perfectly clear on the concept.

The rest of the post will make a lot more sense to you if you tried these exercises yourself first. At the start they seem fairly straightforward, you walk yourself through the implementation of ennumerable methods on array in JS, like map, filter and concatAll (the latter is the equivalent of flatten in other languages). You then start chaining them... ok, so you have methods that return something and then you chain them, so values get passed from one funciton to another and another, you have no explicit variable assignment. I have seen and done it before, nothing new or unusual here tbh, I expected this from functional approach, but where's the catch, what is it about functional programming that makes it, sort of, 'fashionable'?

So I was working steadily through the exercises, it was all making sense and then I got to the question 12 (if you don't want to do all the exercises, I challenge you to try that one at least). It has a deeply nested data by which the result needed to be filtered before producing the final result. This is the data:

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 }]
      }
    ]
  }
];

You can see two objects inside the movieLists array, I will refer to them as category, both of categories have videos property. That property contains video objects - these are the objects we are most interested in for the purpose of this exercise. As a result we need to get the following array of objects:

[
  {"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" }
  ];

Just to verbalise this, we need to get an array of objects which contain only specific information out of original video objects, based on what is in the boxarts property of the video object is an array of boxart objects and we only want one of those included in the result, the one which is 150px wide. I hope that makes sense... We should break this down a bit:

  1. Get the video objects out of nested data structure into a simpler one
  2. Go over the objects in the new structure and extract only the necessary info

This sounds logical, I will try and follow these general steps. By simpler structure I mean an array which containes the video objects, this shouldn't be so hard. First step, I will map over the movieLists array:

movieLists.map( function( movieList ){
  return movieList.videos;
});

The result of this operation will be two nested arrays of video objects:

[
  [
    {
      "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 }]
    }
  ],
  [
    {
      "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 }]
    }
  ]
]

Next step would be to flatten this, I will use concatAll function, which is defined within the exercise (it is not a native JS funciton, so if you are trying to run this code in your console, you would need to define it first, refer to the previous exercises in the set). And, remember, we are in a functional universe right now, so we will just chain this function to the previous one:

movieLists.map( function( movieList ){
  return movieList.videos;
}).concatAll();

After flattening our nested arrays:

[
  {
    "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 }]
  },
  {
    "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 }]
  }
]

Great, our objects of interest are no longer nested, what's next? The next step on the list above was to go over and get only the data we need, nice! So, as a result I need a new array with new objects contructed from the data of each of the video objects, sounds like map would be good to use here:

movieLists.map( function( movieList ){
  return movieList.videos;
})
.concatAll()
.map( function( video ){
  return {
    id: video.id,
    title: video.title,
    boxart: video.boxarts.filter(function(boxart){
      return boxart.width === 150 })[0].url
    }
});

So inside the second map is where I am constructing the new object. For the boxart property all I am doing is filtering through all the boxarts and getting only the one I want. However, the purpose of filter is to return new array, rather than an object itself, which is why I added a sneaky [0] and only after I call .url on it.

The result is exactly what I wanted:

[
  {
    boxart: "http://cdn-0.nflximg.com/images/2891/DieHard150.jpg",
    id: 70111470,
    title: "Die Hard"
  },
  {
    boxart: "http://cdn-0.nflximg.com/images/2891/BadBoys150.jpg"
    id: 654356453
    title: "Bad Boys"
  },
  {
    boxart: "http://cdn-0.nflximg.com/images/2891/TheChamber150.jpg",
    id: 65432445,
    title: "The Chamber"
  },
  {
    boxart: "http://cdn-0.nflximg.com/images/2891/Fracture150.jpg",
    id: 675465,
    title: "Fracture"
  }
]

We got the result, but at what cost? I felt somewhat dirty for having to use index method. And if you read the instructions for the exercise, you would know that I was not supposed to use it. Well that's ok, because I can always use pop instead, with pop that last map would look like so:

movieLists.map( function( movieList ){
  return movieList.videos;
})
.concatAll()
.map( function( video ){
  return {
    id: video.id,
    title: video.title,
    boxart: video.boxarts.filter(function(boxart){
      return boxart.width === 150 }).pop().url
    }
});

This is less obvious, but its the exact same solution, really. And it's just not good enough, not only because I feel dirty to use [0]/pop() here, but also because our solution relies on the fact that a boxart with width of 150 is present for all the video objects in the original data. If filter doesn't find any items matching the condition it returns empty array. And that means that we are calling [0]/pop() on an empty array we will get undefined, which means we would be calling .url on undefined and this will error, this is really flaky...

Looks like we need to filter the suitable videos first and then get the relevant data. The problem here is that the filter condition is not based on the value of one of the video object's own properties, but rather on the value within the value for video's boxarts property... So, if we try to filter, the condition for the filter is going to be long at best. Ok, something like this:

movieLists.map( function( movieList ){
  return movieList.videos;
})
.concatAll() // upto this point exactly as previously
.filter( function( video ){
  return video.boxarts.filter(function(boxart){
    return boxart.width === 150
  }).length != 0;
});

The condition inside the first filter is filtering again through all the values within boxarts and then checking that the length of that result of this inner filter is not 0, we have boxart 150px wide present. If our data is not consistent, ie some don't have the boxart in the required dimensions, only after a step like this it would be safe to use the map we used previously to get the results:

movieLists.map( function( movieList ){
  return movieList.videos;
})
.concatAll()
.filter( function( video ){
  return video.boxarts.filter(function(boxart){
    return boxart.width === 150
  }).length != 0;
})
.map( function( video ){
  return {
    id: video.id,
    title: video.title,
    boxart: video.boxarts.filter(function(boxart){
      return boxart.width === 150 }).pop().url
    }
});

This is very step by step way of getting result, but it still doesn't make me feel less dirty. There must be a way to make it more consize. Let's rewind and come back to the step where we have just flattened our nested arrays:

movieLists.map( function( movieList ){
  return movieList.videos;
})
.concatAll()

to be continued...

So I think I am in love with functional and I hold this guy, and also this guy responsible!