Question: Using functional programming properly: how do I use .map() efficiently here?

Question

Using functional programming properly: how do I use .map() efficiently here?

Answers 5
Added at 2017-01-05 16:01
Tags
Question

I've got two arrays: hobbieList and hobbieTypes. I want to separate the elements in hobbieList, depending on what's the value of their index, in the hobbieTypes array. Something like this:

hobbieList = ["Rock", "Pop", "Surf", "Blues", "Soccer"];
hobbieTypes= ["music", "music", "sport", "music", "sport"];

... so the typical apporach with imperative programming and using fors, would be:

let musicHobbies = [];
for(let i=0;i<hobbieList.length;i++){
   if(hobbieTypes[i] === "music"){
      musicHobbies.push(hobbieList[i]);
   }
}
// Now "musicHobbies" has only the hobbies which type is "music".

But I want to do this with functional programming, using the map() function, and trying to keep the code as short and efficient as possible. Something like this:

let musicHobbies = 
     hobbieList.map( (elem, ind) => (hobbieTypes[ind] === "music") ? elem : null;

I think it looks nice, but there is a problem: the null positions are not erased, so the array shows a "null" where the condition is not satisfied.


The second apporach I could get into, which works nicely but I don't know if it's the proper way to code in a functional way, is this:

 let musicHobbies = [];
 rawHobbies.map( (elem, ind) => (hobbiesTypes[ind] === "music") ? musicHobbies.push(elem) : elem);

Is this second solution, well designed? Or should I change something? And also, should I use the array that the map() function returns (as in case 1), or is it ok if I just get my result in an external array (like in case 2)?

Thank you for your help!

EDIT:

What about this other approach?

      rawHobbies.map( (elem, ind) => {
        switch(hobbiesTypes[ind]){
           case "music":    this.hobbies.music.push(elem); break;
           case "plan":    this.hobbies.plans.push(elem); break;
           case "hobbie":  this.hobbies.hobbies.push(elem); break;
        }
      });

I guess I'd do this better with forEach. Do you think I should use this, use this with forEach, or do something like this...?

this.hobbies.music = rawHobbies.filter( (x,i) => hobbieTypes[i] == "music");
this.hobbies.plans = rawHobbies.filter( (x,i) => hobbieTypes[i] == "plan");
this.hobbies.hobbies = rawHobbies.filter( (x,i) => hobbieTypes[i] == "hobbie");

I'm not pretty sure which one of these options would work the best. Opinions?

EDIT 2:

I ended up using imperative programming for perfomance purposes, but thank you all for the different approaches I can use with functional programming. My final solution is just a simple foor loop:

     for(let i=0;i<rawHobbies.length;i++){
          switch(hobbiesTypes[i]){
           case "music":   this.hobbies.music.push(rawHobbies[i]); break;
           case "plan":    this.hobbies.plans.push(rawHobbies[i]); break;
           case "hobbie":  this.hobbies.hobbies.push(rawHobbies[i]); break;
        }
     }
Answers
nr: #1 dodano: 2017-01-05 17:01

I would just use the filter() method:

const hobbieList = ["Rock", "Pop", "Surf", "Blues", "Soccer"];
const hobbieTypes = ["music", "music", "sport", "music", "sport"];
console.log(hobbieList.filter((x, i) => hobbieTypes[i] === "music"));

As for your edit: I think that using filter() is much cleaner than map() (which you're actually using as forEach()). This might not be the fastest way, but it's definitely more functional.

nr: #2 dodano: 2017-01-05 17:01

Map is used to convert something in something else and not to get element that match a condition. In this case you have to use a filter function.

nr: #3 dodano: 2017-01-05 17:01

After your edit, it looks like you want something more like groupBy.

You can achieve that with .reduce(). That should work for you:

const hobbieList = ["Rock", "Pop", "Surf", "Blues", "Soccer"];
const hobbieTypes= ["music", "music", "sport", "music", "sport"];
var groupedHobbies = hobbieTypes.reduce( (acc, item, index) => { 
        acc[item] = [...(acc[item] || []), hobbieList[index]];
        return acc;
 }, {});

console.log(groupedHobbies);


Usually you want the least side effects as possible and to not do a lot of things at the same time on your map function (which should Map/Convert/Transform each element to something new, without changing the original). By having small functions you can combine them later (you will probably read more about that if you keep following the functional approach)

I would you recommend to use the .filter() method instead of the .map().

Something like that:

hobbieList.filter((elem, ind) => hobbieTypes[ind] === "music")

which will return you an array like:

[
  "Rock",
  "Pop",
  "Blues"
]
nr: #4 dodano: 2017-01-05 17:01

What you seem to want to do is to group hobbies by type. What should the output of this grouping process be? Creating separate variables of lists of hobbies for each type is not very scalable. It's going to be better to create an object, whose keys are types, and whose values are lists of hobbies of that type:

hobbiesByType = {
  music: ["Rock", "Pop", "Blues"],
  sport: ["Surf", "Soccer"]
};

Now I can refer to all the music hobbies as hobbiesByType.music.

What should the input to this grouping process be? Currently you have two parallel arrays:

hobbieList = ["Rock", "Pop", "Surf", "Blues", "Soccer"];
hobbieTypes= ["music", "music", "sport", "music", "sport"];

but this is not the best way to represent this information. Any time I want to check something, I have to reference both arrays. I may have to iterate across them in parallel. It would be much better to have a single representation, and a usual approach would be an array of objects:

hobbies = [
  {name: "Rock", type: "music"},
  ...
];

Now let's think about how to do the grouping. You will find many examples and approaches here on SO. Here's a very basic one using for loops:

var hobbiesByType = {};

for (var i = 0; i < hobbies.length; i++) {
  var hobby = hobbies[i];
  var hobbyName = hobby.name;
  var hobbyType = hobby.type;
  if (!(hobbyType in hobbiesByType)) hobbiesByType[hobbyType] = [];
  hobbiesByType[hobbyType].push(hobby.name);
}

But you want to use "functional" array methods. The most compact form of the above would use reduce and go like this:

hobbiesByType = hobbies.reduce((result, hobby) => {
  if (!(hobby.type in result)) result[hobby.type] = [];
  result[hobby.type].push(hobby.name);
  return result;
}, {});

That's all ES5, not ES6. You could streamline it a bit using ES6 parameter destructuring:

hobbiesByType = hobbies.reduce((result, type, name}) => {
  (result[type] = result[type] || []).push(name);
  return result;
}, {});

However, rather than creating arrays of the name of the hobby under each type, it is likely to be better to create arrays of the hobby objects themselves. Then if hobbies ever contain additional information--skillLevel perhaps--then you easily can a access that from the grouped representation.

By the way, the singular form is "hobby", not "hobbie".

nr: #5 dodano: 2017-01-06 06:01

Using variant data as object keys would be a terrible misuse of objects. Instead, a Map would make a much better choice for your desired data.

A zip function in combination with Array.prototype.reduce will produce a very usable result

let hobbyList = ["Rock", "Pop", "Surf", "Blues", "Soccer"]
let hobbyTypes= ["music", "music", "sport", "music", "sport"]

const zip = ([x,...xs], [y,...ys]) => {
  if (x === undefined || y === undefined)
    return []
  else
    return [[x,y], ...zip(xs, ys)]
}

let result = zip(hobbyTypes, hobbyList).reduce((acc, [type, hobby]) => {
  if (acc.has(type))
    return acc.set(type, [...acc.get(type), hobby])
  else
    return acc.set(type, [hobby])
}, new Map())

console.log(result.get('music'))
// => [ 'Rock', 'Pop', 'Blues' ]

console.log(result.get('sport'))
// => [ 'Surf', 'Soccer' ]

If you really must use an object, though you really shouldn't be, you can do so using map.entries and Array.from.

Array.from(result.entries())
  .reduce((acc, [type, hobbies]) =>
    Object.assign(acc, { [type]: hobbies }), {})

// => { music: [ 'Rock', 'Pop', 'Blues' ], sport: [ 'Surf', 'Soccer' ] }
Source Show
◀ Wstecz