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

—Wednesday, December 07 2005

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. ptype: http://prototype.conio.net/ enum: http://www.rubycentral.com/ref/ref_m_enumerable.html tc: http://dev.conio.net/repos/prototype/test/ Sam: http://project.ioni.st/

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!