Inject and Pluck: The Secret Sauce Behind Prototype's Enumerable

—Thursday, December 28 2006

No misspelling in that title. Two of the most underused, albeit powerful, methods of Prototype are pluck and inject. Pluck provides an easy way to get at data, and inject is like the duct tape of Prototype—applicable in a variety of cases. Let’s look at how these methods can help tidy up our code and make our life a little easier.

Setting the stage

We’re the teacher at Javascript High and our students have just taken an exam. We want to perform a variety of operations on the results of this exam; get the scores; get the average grade; see who got the bonus question correct; and so on. We’ve just got the results back and here’s what we have:

var Results = [
  { name: 'Tom',    grade: 80, bonus: false },
  { name: 'Sally',  grade: 92, bonus: true  },
  { name: 'Fred',   grade: 40, bonus: false },
  { name: 'Joe',    grade: 65, bonus: true  },
  { name: 'Justin', grade: 90, bonus: false },
  { name: 'Dan',    grade: 85, bonus: false },
  { name: 'Angie',  grade: 99, bonus: false }  
];

And we’ll begin with an Exam class stub:

var Exam = Class.create();
Exam.prototype = {
  initialize: function(results) {
    this.results = results;
  }
};

Extracting data using pluck

We want to get the grades in an array because we’re gonna want to get the average grade later on. How might we do that? Your first instinct might be to pop a method like this into your exam class:

  // WRONG
  grades: function() {
    return this.results.map(function(result) {
      return result.grade;
    });
  }

While the above code works, and works well, Prototype has a tool for this type of operation: pluck. Pluck accepts one parameter, the name of the key in which you wish to extract a value from. We have an array of objects, and we want to get the grades by plucking the grade value from each student. Here it is:

  // CORRECT
  grades: function() {
    return this.results.pluck('grade');
  }

When we invoke pluck on an array of objects, it will iterate over those objects grabbing the value in each one corresponding to the key you gave it. Simple, one-liner! We can do this for students if we wanted to get the student names, and so on.

The all mysterious inject

Inject owes a lot of it’s ambiguity to it’s very name “inject”. In the real world, you could think of inject as a snowball-effect.

Inject owes a lot of it’s ambiguity to it’s very name “inject”. In the real world, you could think of inject as a snowball-effect. Assuming you start with a tiny ball of snow, and you begin to pack on more snow, forming a bigger snowball. Each time you pack on more snow, your original snow ball grows bigger. Your accumulating a result by injecting more snow into the results of your previous snowball.

The canonical example for inject is to get the sum of items in an array:

// Returns 15
[1, 2, 3, 4, 5].inject(0, function(sum, value) {
  return sum + value;
});

Basically this amounts to 0 + 1 + 2 + 3 + 4 + 5. The first value is the memo, and the results snowball into a collective sum.

Getting a grade average combining pluck and inject

Now that we have a better understanding of inject, let’s combine it and pluck to get an average grade:

  average: function() {
    return Math.round(this.grades().inject(0, function(sum, value) {
      return sum + value;
    }) / this.results.length);
  }

The above code amounts to Math.round(sum / length). We’re grabbing the grades— this.grades(), which we previously wrote, and getting a sum of those grades using inject, then we divide those grades by the length of the results, getting an average. In the case of our students, the average grade was 79. Needless to say, some of our students need to study.

Who got the bonus question right?

Inject is also a great way to build arrays and objects. What if we wanted to get the names of the students who correctly answered the bonus question? You might be tempted to write something like this:

  // WRONG
  gotBonus: function() {
    var students = []
    this.results.each(function(student) {
      if(student.bonus) students.push(student.name);
    });
    return students;
  }

Again, the code above works, but it’s about the nastiest way to write it. It can be done in far fewer lines of code using inject:

  // CORRECT
  gotBonus: function() {
    return this.results.inject([], function(array, student) {
      return student.bonus ? array.concat([student.name]) : array;
    });
 

Inject takes care of creating and building our anonymous array and we just return the results of inject as our final outcome. In the case of our students, we’d get back the array [“Sally”, “Joe”]. We pass an array as the memo (lump of snow) and if the student got the bonus question right, we pack on a little more snow to our snowball, and if they got it wrong, we just leave our snowball as is and pass it back.

It should be noted that if all we wanted to do was return the original student object back (not just their name), we can use select/findAll:

 // Would return our original student objects for Sally and Joe, not just their name
  gotBonus: function() {
    return this.results.select(function(student) {
      return student.bonus;
    });
  }

Refactoring a bit

Wait! We just realized we want to add passed and failed methods, but if a student got the bonus question right, it should add five points to their grade. This means we’ll need to refactor some things before we end up failing students. But first, lets look at the current state of our class:

var Exam = Class.create();
Exam.prototype = {
  initialize: function(results) {
    this.results = results;
  },
  
  average: function() {
    return Math.round(this.grades().inject(0, function(sum, value) {
      return sum + value;
    }) / this.results.length);
  },
  
  students: function() {
    return this.results.pluck('name');
  },
  
  grades: function() {
    return this.results.pluck('grade');
  },
  
  gotBonus: function() {
    return this.results.inject([], function(array, student) {
      return student.bonus ? array.concat([student.name]) : array;
    });
  }
};

Modify one or many

It’s apparent that we’ve seriously screwed up. It’s looking like we’re gonna have to refactor a couple methods, lumping in additional code to account for our mistake. Not so fast! The only thing we really need to do is modify the initial results passed to our exam to account for the bonus question. This means we can leave our other methods untouched and we don’t have to perform the conversion in every method that accesses student grades.

  initialize: function(results) {
    this.results = results.collect(function(student) {
      return student.bonus ? (student.grade += 5, student) : student;
    });
  }

Who passed, who failed, who did good, who did bad?

Now that we’ve sorted out the bonus question debacle, lets see who passed and who failed:

  passed: function() {
    return this.results.inject([], function(array, student) {
      return student.grade >= 70 ? array.concat([{name: student.name, grade: student.grade}]) : array;
    });
  },
  
  failed: function() {
    return this.results.inject([], function(array, student) {
      return student.grade < 70 ? array.concat([{name: student.name, grade: student.grade}]) : array;
    });
  }

Those who scored 70 or above are considered to have passed, those who didn’t failed. We again use inject to build a new array of results depending on if they passed or failed. The new objects in the returned array have the students name and grade.

I could’ve abstracted the inject code into another method to keep me from duplicating it, but for the sake of completeness I wanted to show them in full. It should also be noted that if I just wanted to get back an array of the original objects (students), I could use select/findAll to do a simple conditional find.

Who scored high, who scored low?

Finally, as an additional exercise, we want to get the student with the lowest grade, and the student with the highest grade.
If I just wanted to get back the value of the highest and lowest grades, I could use min and max, but I want to get back the object that contains the highest/lowest grade so I’m gonna have to use some additional trickery.

The method we’re looking for is sortBy which accepts a block/iterator, and the results of that iterator are in turn, used to sort the resulting array. So, here it is:

  highestGrade: function() {
    return this.results.sortBy(function(student) {
      return student.grade;
    }).last();
  },
  
  lowestGrade: function() {
    return this.results.sortBy(function(student) {
      return student.grade;
    }).first();
  }

The grades are sorted from low to high, so the first item in the resulting array after sorting is the student with the lowest grade, and the last item is the student with the highest grade. Again, this could be consolidated to reduce duplication using something like partition, but to help you visualize it without the extra cruft I chose not too.

And thats a wrap

We’ve created a pretty complete Exam class and explained how to use two powerful methods– inject and pluck, and for good measure, we threw in a sortBy example. Hope you learned a thing or two, here is our final class:

var Exam = Class.create();
Exam.prototype = {
  initialize: function(results) {
    this.results = results.collect(function(student) {
      return student.bonus ? (student.grade += 5, student) : student;
    });
  },
  
  average: function() {
    return Math.round(this.grades().inject(0, function(sum, value) {
      return sum + value;
    }) / this.results.length);
  },
  
  students: function() {
    return this.results.pluck('name');
  },
  
  grades: function() {
    return this.results.pluck('grade');
  },
  
  gotBonus: function() {
    return this.results.inject([], function(array, student) {
      return student.bonus ? array.concat([student.name]) : array;
    });
  },
  
  passed: function() {
    return this.results.inject([], function(array, student) {
      return student.grade >= 70 ? array.concat([{name: student.name, grade: student.grade}]) : array;
    });
  },
  
  failed: function() {
    return this.results.inject([], function(array, student) {
      return student.grade < 70 ? array.concat([{name: student.name, grade: student.grade}]) : array;
    });
  },
  
  highestGrade: function() {
    return this.results.sortBy(function(student) {
      return student.grade;
    }).last();
  },
  
  lowestGrade: function() {
    return this.results.sortBy(function(student) {
      return student.grade;
    }).first();
  }
};