December 28th, 2006

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

15 comments on 2629 words

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();
  }
};

Discussion

  1. Dean Edwards Dean Edwards said on December 28th
    inject is basically the same as reduce (of Google map/reduce fame) which is basically the same as foldl in functional languages.
  2. kourge kourge said on December 28th

    inject() is a really nice way to do something like sum(). Declaring Array.prototype.sum is often very tempting, but it’s one of those tricky ones that you wish are there but won’t use it that much.

    pre. [1, 2, 3, 4, 5].inject(0, function(x, y) {return x + y;});

    pluck() was the most useful when it used to be that there wasn’t an up() method you could use. Now that there’s invoke('up') there’s no need for pluck('parentNode').

  3. kourge kourge said on December 28th

    Oops. The “pre” part was supposed to be preformatted text.

  4. Justin Palmer Justin Palmer said on December 29th

    Nice tip kourge. I had never thought about using `pluck(parentNode)`. Even though, as you say, it’s a little late to get clued in on that now that we have `up()`.

  5. Peter Bex Peter Bex said on December 29th

    Nice article, let’s hope people will start to dig the functional style of programming a bit more. This will lead to better programs, I’m sure.

    Incidentally, the passed and failed separation can also be done with a built-in function in prototype: [passed, failed] = this.results.partition(function(s) { return s.grade >= 70 });. I don’t really see the need to create new objects just to remove the bonus.

    By using partition instead of inject, you reduce the possibility of errors (what if you accidentally modify the ‘passed’ check without updating the ‘failed’ check too?) and you only need to iterate through the array once instead of twice.

  6. Justin Palmer Justin Palmer said on December 29th

    Peter, unless I’m mistaken destructuring assignment doesn’t work with anything but Firefox 2.

  7. Peter Bex Peter Bex said on December 29th

    I wanted to keep the code concise. Of course, in browsers with older JS versions you’ll need to hand-pick the passed/failed students from the result array.

  8. Tim Tim said on December 29th

    What exactly would you use this for other than that?

  9. kourge kourge said on December 30th

    You can use inject() to concatenate an array of strings or sum up an array of numbers. pluck() is good for getting a property out of a JSON object or an array of DOM nodes.

    For example: ['how', 'are', 'you'].inject(function(x, y) {return x + ' ' + y;});

    This would get all the existing classes applied on span elements: var spanClasses = $$('span').pluck('className').compact();

  10. Ron Derksen Ron Derksen said on January 2nd

    Excellent article. Have to keep this in mind for when I run into something that I can use this for :).

  11. Jakob Kruse Jakob Kruse said on January 11th

    Ofcourse you could also write the gotBonus function using select and pluck:

    @ gotBonus: function() { return this.results.select(function(student) { return student.bonus; }).pluck(‘name’); } @

    That way we avoid the rather quirky looking array concatenation. The same method works even better for passed and failed.

    (hope this ends up looking right, I’m not used to Textile)

  12. Daniel Vandersluis Daniel Vandersluis said on January 11th

    Why not just use select and reject to get the pertinent objects for passed and failed. For example:

    passed: function() { this.results.select(function(student) { return student.grade > 70; }) }

    Then you could use pluck to retrieve the specific field you need, if necessary.

  13. Daniel Vandersluis Daniel Vandersluis said on January 11th

    And… it looks like Jakob Kruse beat me to it. ;)

  14. Jakob Kruse Jakob Kruse said on January 12th

    Daniel, you make up for it with Textile skills I see ;)

  15. Phil Crosby Phil Crosby said on January 14th

    This is obnoxiously well written. Please consolidate posts like this into a book about how to write good Javascript.

Sorry, comments are closed for this article.