December 7th, 2005

Prototype Meets Ruby: A Look at Enumerable, Array and Hash

26 comments on 1943 words

Not to long ago, Prototype implemented some Ruby-like features, most notably Enumerable. This makes Javascript much more pleasant to deal with, but currently there is almost no documentation on how to use these features. Luckily, Sam believes in testing his code and he has some test cases lying around his darcs repository that helped me get up to speed. I’ll try to explain some of the methods in this post.

I’m going to start out with this sample data that all of the examples will be based off of. I’m also using a Javascript Logger that I’m still actively developing and thus is still very much unfinished, but for the moment it can help me get through this article.


var Fixtures = {
  Products: [
    {name: 'Basecamp', company: '37signals',  type: 'Project Management'},
    {name: 'Shopify',  company: 'JadedPixel', type: 'E-Commerce'},
    {name: 'Mint',     company: 'Shaun Inman', type: 'Statistics'}
  ],

  Artist:   ['As I Lay Dying', '36 Crazyfist', 'Shadows Fall', 'Trivium', 'In Flames'],
  Numbers:  [0, 1, 4, 5, 98, 32, 12, 9]

};

var F = Fixtures;

each and friends

I used to find myself writing a lot of for loops. Although, Prototype doesn’t by any means eliminate the need to do for loops, it does give you access to what I consider to be a cleaner, easier to read method in each.


for(var i = 0; i < F.Numbers.length; i++) {
  logger.info(F.Numbers[i]);
}

each allows us to iterate over these objects Ruby style.


F.Numbers.each(function(num) {
  logger.info(num);
});

//Output
0
1
4
5
98
32
12
9

each takes one argument, the iterator or block in Ruby terms. This iterator is invoked once for every item in the array, and that item along with the optional index is passed to the iterator. So if we also needed the index we could do something like the code below.


F.Numbers.each(function(num, index) {
  logger.info(index + ": " + num);
});

//Output
0: 0
1: 1
2: 4
3: 5
4: 98
5: 32
6: 12
7: 9

Hash key/value pairs

Hashes, created by wrapping an Object (associative array) in $H() can have their key/value pairs exposed.


    $H(F.Products[0]).each(function(product) {
      logger.info(product.key + ": " + product.value);
    });

//Outputs
 name: Basecamp
 company: 37signals
 type: Project Management

We can also directly access the keys and values of a Hash without iterating over it.


$H(F.Products[1]).keys();
//Outputs name,company,type 

$H(F.Products[1]).values();
//Outputs Shopify,JadedPixel,E-Commerce

Make sure you don’t have a lapse of reasoning (like I did) and do keys instead of keys().

“this” inside iterators

UPDATE: Gordon commented that this is indeed available inside iterators with the use of Prototypes bind method.

I tried a similar method using bind but was unsuccessful, it turns out I had my syntax wrong. Gordon also made a small mistake in his syntax in that apply is no longer required. Below is the correct syntax that works.


F.Numbers.each(function(num, index) {
  this.otherNumbers(num);
}.bind(this));

So this.otherNumbers(num) will be executed in the scope of the current object which is awesome! This was one of my major gripes against the methods that implement iterators, this is music to my ears. :-)

collect

collect allows you to iterate over an Array and return the results as a new array. Each item returned as a result of the iteration will be pushed onto the end of the new array.


var companies = F.Products.collect(function(product) {
  return product.company;
});
logger.info(companies.join(', '));

// Outputs
// 37signals, JadedPixel, Shaun Inman

You can even join on the end of the block.


return F.Products.collect(function(product) {
  return product.company;
}).join(', ');

include

include allows you to check if a value is included in an array and returns true or false depending on if a match was made. Assuming I put up a form asking the user to name some artist in my iTunes playlist, we could do something like the code below. Prime candidate for some conditional madness.


 return F.Artists.include('Britney Spears'); // returns false thankfully ;-)

inject

inject is good for getting a collective sum from an array of values. For instance, I use it’s Ruby counterpart in a task logging application to add up all the time a user has logged on a project.


var score = F.Numbers.inject(0, function(sum, value) {
  return sum + value;
});
logger.info(score);

//Output 161

The first argument to inject is just an initial value that would be added to the sum, so if we added 1 instead of 0, the output would be 162.

findAll

When given an Array, findAll will return an array of items for which the iterator evaluated to true. Basically, it allows you to build a new array of values based on some search criteria. If we wanted to find all products whose type was “E-Commerce” we could do something like the code below.


var ecom = F.Products.findAll(function(product) {
  return product.type == 'E-Commerce';
});
logger.info(ecom[0].company + " produces " + ecom[0].name);

//Outputs
JadedPixel produces Shopify

Note that even if only one match is made, just as in this case, the result is still returned as an array. In that case, ecom.company would return undefined.

detect

Unlike findAll, detect will only return the first item for which the expression inside the iterator is true. So, if we wanted to find the first number that was greater than 5 we’d do something like the code below.


var low = F.Numbers.detect(function(num) {
  return num > 5
});
logger.info(low);

//Outputs 98

Even though, there are other numbers above 5 in our array, detect only gives us the first match back.

invoke

invoke allows us to pass a method as a string and have that method invoked. For instance, if we wanted to sort our array of artists we’d do something like this:


[F.Artists].invoke('sort')
//Outputs 36 Crazyfist,As I Lay Dying,In Flames,Shadows Fall,Trivium

Why not just use F.Artists.sort? Well, for the example above we could do just that, but here is where invoke shines.


[F.Artists, F.Letters].invoke('sort');
//Outputs 36 Crazyfist,As I Lay Dying,In Flames,Shadows Fall,Trivium,b,c,f,h,m,r

So we invoked sort for each sub-array. Note that the code below will not work.


F.Artists.invoke('sort');

The reason this will not work is because it is taking each item in that array and trying to apply sort to it, thus if we wrote it outright, it would look something like this:


"36 Crazy Fists".sort();

We could however do something like this:


F.Artists.invoke('toLowerCase');
//Outputs 36 crazyfist,as i lay dying,in flames,shadows fall,trivium

Now, what about passing arguments to invoke? It’s fairly simple. The first argument passed to invoke is the method to be invoked, and any other arguments beyond that will be passed as arguments to the invoked method.


F.Artists.invoke('concat', " is awesome ")
//Outputs
36 Crazyfist is awesome ,As I Lay Dying is awesome ,In Flames is awesome ,Shadows Fall is awesome ,Trivium is awesome

Thats a wrap

This is getting to be pretty long, so I’m going to cut it off here and maybe come back later and fill in any holes or discuss some of the other methods. I hope this is enough to get your feet wet. I also encourage you to take a look at Sam’s test cases to find out more about Enumerable, Arrays and Hashes in Prototype.

Happy Prototyping!

Discussion

  1. atmos atmos said on December 7th

    DUDE! Why couldn’t you have been such a positive beacon of light when I was a Memphian! We should grab a coffee or beer when I’m in the area at Christmas time.

    The code blocks are kinda hard to read with your typo theme. Regardless, thanks for the insight in this posting.

  2. Björn Björn said on December 8th

    Ahaa, thanks for the insights! Prototype has a lot of hidden gems it seems ;)

  3. Gordon Gordon said on December 8th

    Very nice overview. One suggestion, though. Prototype adds a ‘bind’ function to the Function object. Bind can enable you to access ‘this’ within your iterator.

    For example, we can convert the following:
    var other = this.otherNumbers;
    F.Numbers.each(function(num, index) {
      logger.info(other.apply());
    });
    

    to this (which is a bit cleaner):

    F.Numbers.each(function(num, index) {
      logger.info(this.otherNumbers.apply());
    }.bind(this));
    
  4. Justin Palmer Justin Palmer said on December 8th

    Gordon, thanks! I tried bind, but I had my syntax wrong, yours makes perfect sense. I’ll update the article to reflect that.

  5. Kyle Kyle said on December 8th

    One word: sick.

    Just earlier I was wishing there was a .each for PHP… now at least I have my beloved .each in Javascript.

  6. Bjorn Bjorn said on December 8th

    Off-topic: When I post comments in Typo-powered weblogs, I often run into issues with my name. It spells “Björn” instead of “Björn”. Is UTF-8 breaking when posting forms through javascript in general, or does it have anything to do with prototype? Anybody knows?

  7. Ezra Zygmuntowicz Ezra Zygmuntowicz said on December 9th

    Justin-

    this is awesome stuff. i wasn’t aware of any of it, me being more of a ruby buff then javascripter. But this changes everything. Love it , keep it coming.

  8. Sam Stephenson Sam Stephenson said on December 9th

    Justin: Great article! Thanks for the write-up.

    Enumerable gets a little bit more interesting in JavaScript because functions (when referenced without trailing parentheses) are just regular objects. So if all you’re doing is passing every element in an array to a function, there’s no need to make a new iterator function to wrap it—just pass the function directly:

    F.Numbers.each(logger.info);

    Be careful, though: if your iterator accepts more than one argument, the second argument will be the index of the array element.

    Other interesting stuff: there’s an Enumerable function called pluck, which is similar to invoke but doesn’t make a function call. So if you have an array of DOM elements and you want to get an array of all those elements’ IDs,

    elements.pluck('id');

    Finally, there’s lots of built-in JavaScript objects which act like arrays (i.e., have a .length property and can be indexed with the [] operator) but aren’t, so they don’t have the Enumerable methods mixed in. A couple of examples of this are the arguments object available in every JavaScript function, and instances of the NodeList DOM class (which you get from stuff like getElementsByTagName).

    Prototype calls these iterables. You can convert any iterable into an array with $A:

    $A(document.getElementsByTagName('tr')).each(...)
  9. Justin Palmer Justin Palmer said on December 10th

    Thanks Sam! Also thanks for the other useful tips, extremely handy and great to know. I had glanced at pluck and seen that you had been making good use of it in Prototype, but never looked into exactly what it did. All the more reason to use Prototype :-D

  10. Lon Lon said on December 11th

    I really don’t understand all the fuzz about this Prototype library…

    Replacing getElementById by $ and a for loop by .each and a fresh, memory leaking, closure isn’t what makes developing javascript applications easier because that’s not the hard part.

    Try abstracting the hard part, not the syntax.

  11. Rich Rich said on December 11th

    Before you spend too much time on the logger, you might want to have a look at MochiKit. He has a nice screencast demo and decent documentation on the site.

    Thanks for the article. -Rich

  12. Jim Geurts Jim Geurts said on December 12th

    Nice overview!

    I could be wrong, but I think there is a small typo with how you define the Fixtures variable. Based off the other examples in the page, it should be defined as “var F = ... ” rather than “var Fixtures = ...”

  13. Justin Palmer Justin Palmer said on December 12th

    Jim, thanks for pointing that out. I had actually come back in my local code and pointed F to Fixtures and forgot to change it in the article.

  14. iiome iiome said on December 12th

    I was reading the whole article thinking this was a new ruby syntax… Those improvements to Javascript are impressive!

  15. Seth Seth said on December 21st

    I wanted to hate prototype.js for a long time, being a heavy DHTML developer for years – but stuff like this is making me love it.

    Thanks for the writeup.

  16. jamund jamund said on December 22nd

    you make my life wonderful

  17. Introspective Introspective said on December 22nd

    Quick comment: the background color for this site is not set, yet all graphics assume it’s white. Looks haenous with my colorscheme. ;)

  18. John Nunemaker John Nunemaker said on January 3rd

    I was just going to go through prototype to learn these when I remembered your article. Nice explanations. Saved me some time having to figure it out.

  19. Mandy Mandy said on January 8th

    Hi Justin,

    I would like to discuss some things with you but they are not related to this article. They are related more to the logger.js script that you were writing. I wanted to send you an email but I just couldn’t track down your email address. I have no idea where it is hidden :) Could you please let me know…

    Regards, Mandy.

  20. Rich Thornett Rich Thornett said on February 20th

    Great tutorial! Had a follow-up question: is it possible to break and continue when iterating with ‘each’?

  21. James James said on March 6th

    Yeah, – is there any way to make ‘break’ or ‘return; or ‘continue’ work, in an ‘each’ function?

  22. Blair Blair said on March 17th

    Regarding the break or return, I think that each just invokes the function you provide for each element in the enumerable object so you could ‘continue’ by just returning the function in a given case. but I don’t see how breaking out of the entire loop not just the current iteration would work given its current structure.

  23. Justin Palmer Justin Palmer said on March 17th

    Rich, look at $break and $continue. This should be what your looking for.

  24. Rob Wilkerson Rob Wilkerson said on March 31st

    Justin -

    Would you mind providing more information on $break and $continue? I can’t seem to turn up enough information to do anything with these…functions?

    Thanks.

  25. David David said on April 21st

    Rob Wilkerson: arraylist.each(function(value) { if(value == 1) throw $break; });

  26. Mark Mark said on July 4th

    Great post! Just what I was looking for.

    Also, I love the blog design – pleasing on the eyes.

Sorry, comments are closed for this article.