Javascript Functional programming with Underscore or ECMAScript5

Christian Harms's picture

Intro: A little excursion to code more functional with javascript. It contain examples with the classic for-loop-code, the more functional variant with Underscore.js and the HTML5 functional enhancements. It does not explain the basic idea of functional programming - it shows the differences in code examples.

motivation

Last week I got access on the address book from Matthias' well known entry Jennifer. But I had no time to scan all the other girls addresses. With many lines of source code filled with for-loop-code - no way. I have to find a smarter coding way to scan the address book.

Anonymized address book data looks like this - containing many female and male addresses.

  1. addressbook = [  {gender: "female", nickName: "Jenny1"},
  2.                  {gender: "male",   firstName: "Chris"}
  3.               ];

It's time to code more functional with javascript (last year's entry was about functional programming with python). First I will show the normal loop-variant. After the old variant I will show the functional variant with modern browser (only internet explorer doesn't support it now, IE9 will). Or if you use more recent browser you can use the examples I made in Underscore.js (the global object is a _ ). Functional code is shorter, but does it perform? I added the micro benchmarks for all examples created with Jslitmus.js with some surprising results. All benchmark times are generated on my old ASUS eeeBox with an Intel Atom N270 CPU.

Are there matching objects?

The first addresses in the list contains only girls: I want to find out if at least one male address is in the address book. The functional helper is called "isMale(address)".

  1. function isMale(adr) {
  2.     return (adr.gender === "male");
  3. }
  4.  
  5. function findMan_Loop() {
  6.     var i=0, len=addresses.length;
  7.     for (;i<len; i++) {
  8.         if (isMale(addresses[i])) {
  9.             return true;
  10.         }
  11.     }
  12.     return false;
  13. }

If one of the function call of isMale returns true, the result of function findMan_Loop is true. A little bit more functional with ECMAScript 5 or Underscore.js looks smarter and produce the same result. You have to read the line like "return true if any of isMale-call with an item from addresses returns true".

  1. function findMan_some() {
  2.     return addresses.some(isMale);
  3. }
  4.  
  5. function findMan_Underscore() {
  6.     return _.any(addresses, isMale);
  7. }

The normal loop is faster than the functional variants - browser optimize simple loops more than the function variant.

Check every entry

The other check would be: Does the addressbook contain any women?

  1. function onlyWomen() {
  2.     var i=0, l=addresses.length, result=true;
  3.  
  4.     for (;i<l && result;i++) {
  5.         result &= isFemale(addresses[i]);
  6.     }
  7.     return result;
  8. }

With this variant I have to check every entry. If every call of the function isFemale is true the result should be true. You have to read it as "return true if all isFemale calls with the items from addresses return true".

  1. function onlyWomen_every() {
  2.     return addresses.every(isFemale);
  3. }
  4.  
  5. function onlyWomen_Underscore() {
  6.     return _.all(addresses, isFemale);
  7. }

Same performance like the first example.

Get all entries with given filter

But how to get all the women out of the address book? I have to check for every entry and copy the matching entries in a new result array.

  1. function getWomans_Loop() {
  2.     var i=0; l=addresses.length, res = [];
  3.     for (;i<l;i++) {
  4.         if (isFemale(addresses[i])) {
  5.             res.push(addresses[i]);
  6.         }
  7.     }
  8.     return res;
  9. }

The functional variant is named filter and the code is shorter:

  1. function getWomans_filter() {
  2.     return addresses.filter(isFemale);
  3. }
  4.  
  5. function getWomans_Underscore() {
  6.     return _.filter(addresses, isFemale);
  7. }

I love browser optimizations and micro benchmarks. Chrome is 2x faster with the loop and array.push, Firefox is 2x faster with filter. And my old Opera (10.10 - not 9.80 like his UserAgentString) is 50x times slower than Chrome!

Expand the data with map

Matthias has described how to generate the display name from the different attributes. I want to expand the address book with the new attribute. The function should iterate over all elements, expand every entry with the attribute displayName and return a new array.

  1. function copyDisplayName(adr) {
  2.   return { firstName: adr.firstName,
  3.            nickName: adr.nickName,
  4.            gender: adr.gender,
  5.            displayName: displayName(adr)
  6.   };
  7. }
  8.  
  9. function copyBookWithDisplayName_Loop() {
  10.     var i=0; len=addresses.length, res =  new Array(len);
  11.     for (; i<len; i++) {
  12.         res[i] = copyDisplayName(addresses[i]);
  13.     }
  14.     return res;    
  15. }

The result is a new array with all entries and the attribute displayName. The functional variant call the function copyDisplayName for every item and return a new array.

  1. function copyBookWithDisplayName_map() {
  2.     return addresses.map(copyDisplayName);
  3. }
  4.  
  5. function copyBookWithDisplayName_Underscore() {
  6.     return _.map(addresses, copyDisplayName);
  7. }

One performance error is in the last example: I dont need to copy the complete array the original list can manipulated.

Expand the data with forEach

In this last test I created an new Array. Now I will add the displayName member directly in the address book.

  1. function setDisplayName(adr) {
  2.   adr.displayName = displayName(adr);
  3. }
  4.  
  5. function setDisplayName_Loop() {
  6.     var i=0; len=addresses.length;
  7.     for (; i<len; i++) {
  8.         setDisplayName(addresses[i]);
  9.     }
  10. }

The functional variant is shorter and more elegant. It call the setDisplayName to modify the given object in the array directly.

  1. function setDisplayName_forEach() {
  2.     addresses.forEach(setDisplayName);
  3. }
  4.  
  5. function setDisplayName_Underscore() {
  6.     _.each(addresses, setDisplayName);
  7. }

Now the loop is slower than the functional variants - fine.

conclusion

Have fun with running the benchmarks in your browser - all the slow timings are made in my ASUS eeeBox with the Intel Atom N270 and Ubuntu!

If you (and your coworkers) like the functional coding style and using an javascript environment supporting it - use it. With performance issues you should try it which variant works better!

links

AttachmentSize
functional.js.txt3.16 KB
underscore-min.js.txt8.35 KB
JSLitmus.js.txt19.81 KB

Comments

Christian Harms's picture

Because the javascript performance is limited on mobile devices, here some benchmarks from Safari auf iPhone 4, Frojo auf Nexus and Android 2.1 auf dem Samsung Galaxy:

 Twitter Trackbacks for Javascript Functional programming wi's picture

[...] Javascript Functional programming with Underscore or ECMAScript5 | united-coders.com united-coders.com/christian-harms/javascript-functional-programming-with-underscore-or-ecmascript5 – view page – cached Intro: A little excursion to code more functional with javascript. It contain examples with the classic for-loop-code, the more functional variant with Underscore.js and the HTML5 functional enhancements. It does not explain the basic idea of functional programming - it shows the differences in code examples. Tweets about this link [...]

Theresa Jones's picture

I do like the idea behind this underscore.js library, but I like jPaq ( http://jpaq.org/ ) a little bit better. The reason is the fact that jPaq gives you the ability to download your own custom build of the library, based on which objects, classes, and functions you need. Therefore, unlike what is true in all of the other libraries, jPaq gives you the ability to download a library made for your project. For this reason, I think I am going to start using jPaq instead of Underscore.