JavaScript
Article

Quick Tip: Stop Writing Loops and Start Thinking with Maps

By Jezen Thomas

This article was peer reviewed by Chris Perry, Marc Towler, Simon Codrington and Tim Evko. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

There comes a time in the learning path for most programmers when they discover a function called map. Up until discovering the map function, you might use a for loop whenever you needed your machine to perform some action many times. In the common case, that action would be transforming some data.

Imperative

For example, a salesperson on your team hands you a big list of email addresses. Not a great deal of care was taken in validating the email addresses as they were coming in, so some of them are uppercase, some of them are lowercase, and some of them are a mix of the two. The for loop approach to transforming the data looks like this:

var mixedEmails = ['JOHN@ACME.COM', 'Mary@FooBar.com', 'monty@spam.eggs'];

function getEmailsInLowercase(emails) {
  var lowercaseEmails = [];

  for (var i = 0; i < emails.length; i++) {
    lowercaseEmails.push(emails[i].toLowerCase());
  }

  return lowercaseEmails;
}

var validData = getEmailsInLowercase(mixedEmails);

This approach works, but it involved a painful amount of ceremony to achieve what is in reality a simple and common operation. Our function with the for loop encodes so much detail that we didn’t intend to express. A few sore points:

  • We told the machine that it needs to create a temporary list that it copies email addresses to.
  • We told the machine to first count how many email addresses we want to transform, and then move through the list of email addresses exactly that number of times.
  • We told the machine to create a counter so it knows what position of the email address list its operating on.
  • We told the machine which direction it should count in, which implies that ordering is important at this stage — which it isn’t.

This is the imperative approach to programming. We are dictating to the machine how it should do its job.

Confused

We want to clean up the previous approach, so we reach for the map function. As we read through any documentation for the map function, we see words like “array”, “each”, and “index”. This would suggest we could treat map as a slightly less ceremonious for loop, and indeed we can. Let’s change our original function.

var mixedEmails = ['JOHN@ACME.COM', 'Mary@FooBar.com', 'monty@spam.eggs'];

function getEmailsInLowercase(emails) {
  var lowercaseEmails = [];

  emails.map(function(email) {
    lowercaseEmails.push(email.toLowerCase());
  });

  return lowercaseEmails;
}

var validData = getEmailsInLowercase(mixedEmails);

This works, and is cleaner than the for loop approach. Aside from there being fewer characters in the code snippet, we’re not telling the machine how to keep track of indexes or which direction it should work through our list.

However, this is not enough. This is still the imperative approach to programming. We are still dictating far too much. We are concerning ourselves with details we need not concern ourselves with, and we are holding our computer’s hand every step of the way.

Declarative

What we need is to change the way we think about the data transformation. We don’t think “Computer, I need you to take the first element of this list, then lowercase it, then push it to this other list, then return the list”. Instead we think “Computer, I have a list of mixed-case email addresses, and I need a list of lower-case email addresses. Here’s the function that does lowercasing.

var mixedEmails = ['JOHN@ACME.COM', 'Mary@FooBar.com', 'monty@spam.eggs'];

function downcase(str) {
  return str.toLowerCase();
}

var validData = mixedEmails.map(downcase);

It’s not a stretch to argue that this is more readable to a human, and that’s what programming is all about: expressing ideas to other humans, be they other developers or your future self. The above snippet says “Our valid data is our mixed emails list mapped over the downcase function”.

Expressing ideas at such a high level like this is a core tenet of the school of functional programming, and that’s essentially what we’re doing. Complex programs are built by combining simple components which have a single responsibility and are easy to understand.

There are several further advantages to this approach. In no particular order:

  • Our lowercasing function provides the simplest-possible interface; a single value in, and a single value out.
  • There are fewer moving parts, so our logic is easier to understand, easier to test, and is less likely to break.
  • Our logic does just one thing, so it’s easy to reuse and combine with other functions in order to express more complex ideas.
  • It’s not uncommon for the size of a codebase to shrink dramatically when going down this declarative road.

Although the use of an anonymous function as the first argument to map() is common, I recommend pulling functions out and giving them meaningful names. This helps to document your intent with the function, so another developer later can understand what the method does by reading the name instead of having to
mentally parse the implementation.

Browser Support

The native map() method is defined in the ECMAScript 5 specification and has good browser support. If you need to support an Internet Explorer version earlier than 9, you can introduce a polyfill or use a library like Underscore or Lodash.

Performance

In the vast majority of cases, the choice between the map function and a for loop will have no performance implications in real-world code. The for loop is marginally faster, but the difference is not worth considering unless you’re writing some form of graphics or physics engine, and even then it doesn’t make sense to introduce this optimization before profiling your performance-critical code so you have some hard data to work on.

Wrapping Up

The functional approach of separating logic into simple pure methods and applying those methods to data structures will make your code more concise, more robust, and easier to understand. The concept is general, and more general concepts allow for greater code reuse. Learning to think this way will improve not only your JavaScript, but your work with most other programming languages too; you can apply this approach in Ruby as readily as you can in Haskell.

So, next time you reach for a for loop, reconsider. Bear in mind that the data structure you begin with doesn’t necessarily need to be a flat array; you can start with an object, pull out its values, then map a function over that and end by sorting the resulting array. You can even use a library such as Underscore to map over object preserving the keys.

Can you think of any more creative ways of using the map() function? Experiment, and watch your code shrink.

  • http://cashpath20.com JudyJMena

    .❝my neighbor’s mother is making $98 HOURLY on the
    internet❞….

    A few days ago new McLaren F1 subsequent after earning 18,512$,,,this was my
    previous month’s paycheck ,and-a little over, $17k Last month ..3-5 h/r of work a day ..with extra
    open doors & weekly paychecks.. it’s realy the
    easiest work I have ever Do.. I Joined This 7 months ago and now making over
    $87, p/h.

    Learn More right Here….website on my PrroFile
    +fdsdfd

  • İsmail Şener

    It’d be very helpful article. Thanks a lot, it gave me a 1 level in this environment :p

  • ShadowCodex

    This was a really good article, and a lot of people need to read this to up their game a little.

  • Adam Reineke

    If you don’t need the return values from your callback function, skip .map and use .forEach, which has similar support but should drop the overhead of building an array the callback return values.

    • Olu O

      Just came here to say that. The .map higher order function makes sense in the last case as you’re returning a new array, but in the first two examples, at least the way the code is written, a .forEach function is really what you should be using if you’re pushing values to the ‘lowerCaseEmails’ array.

  • Edwin Reynoso

    Under the Confused section why not:

    function getEmailsInLowercase(emails) {
    return emails.map(function(email) {
    return email.toLowerCase();
    });
    }

    • http://jezenthomas.com/ Jezen Thomas

      At this point, why wrap it in a function at all?

      • Edwin Reynoso

        Because you can still pass an Array of emails to it. My point was if you’re going to use the `map` method why create an array and push to it, when `map` basically already does that

        • http://jezenthomas.com/ Jezen Thomas

          > Because you can still pass an Array of emails to it.

          You can map an array directly. You don’t need to wrap `map` in a function to be able to pass it an array.

          > if you’re going to use the `map` method why create an array and push to it, when `map` basically already does that

          Totally :) That’s one of the main points of the article.

          • Edwin Reynoso

            > You can map an array directly. You don’t need to wrap `map` in a function to be able to pass it an array.

            What I mean is that the function is lowercasing each string, yes you just call `map` on any array, but the point of the function is to not repeat yourself on lowercasing each string:

            [’email1′, ’email2′, ’email3′].map(function(email) {

            return email.toLowerCase();

            });

            // doing it again

            [’email1′, ’email2′, ’email3′].map(function(email) {

            return email.toLowerCase();

            });

            Create the function:

            function getEmailsInLowerCase(emails) {

            return emails.map(function(email) {

            return email.toLowerCase();

            });

            }

            getEmailsInLowerCase(['email1', 'email2', 'email3']);
            getEmailsInLowerCase(['email1', 'email2', 'email3']);
            getEmailsInLowerCase(['email1', 'email2', 'email3']);

            The point is to make that function shorter, but not as short as the last example:

            ['email1', 'email2' ,'email3'].map(toLowerCase);

          • http://jezenthomas.com/ Jezen Thomas

            In your first snippet, you have created two anonymous functions that do the same thing, which is repetitive.

            In your second snippet, you have coupled your function with the context of emails. What happens if you want the same logic within a different context? What if you were lowercasing URLs?

            The `map` function is a general concept, and this generalisation allows you to stop repeating yourself.

            > The point is to make that function shorter, but not as short as the last example

            What’s wrong with the last example being so short?

          • Edwin Reynoso

            The first snippet I repeated myself on purpose, showing the point of the function. All I’m trying to say is that in your second function you could make it shorter, and even shorter in your last

          • http://jezenthomas.com/ Jezen Thomas

            Your third snippet doesn’t work in JavaScript. It works in Ruby and a bunch of other languages though.

            In Ruby:

            ['FOO', 'BaR', 'baz'].map(&:downcase)

          • Edwin Reynoso

            The third snippet is representing what was done at the end of this article, for some reason I can’t open the article but still can reply from disqus. I wasn’t trying to pass in the method to call and map

          • Nathan Strutz

            It’s a bit more complex with Javascript. toLowerCase is a String member function, so you have to tell map to “call” the toLowerCase function:


            ['A', 'b', 'C'].map(Function.prototype.call, String.prototype.toLowerCase);

            Alternatively, you could define an independent toLowerCase function and map that:


            var tlc = function(str){return str.toLowerCase();}
            ['A', 'b', 'C'].map(tlc);

    • nnnnnn321

      Why not? Because the Confused section is *deliberately* failing to use .map() properly. It’s an example of what somebody might do if they don’t properly understand how to use it. (Though I disagree with the author that doing this is “cleaner” than a for loop. Using .map() properly is cleaner, but this is messier.)

  • Bob Jones

    Handy. It’s also important to realize that you can’t “break” from .map() or .forEach(). Lodash does have some functions that allow you to “break”. But generally, you’ve committed to the whole array with these.

    • Rafael Bitencourt

      “…you’ve committed to the whole array with these”. Don’t forget you can always run .filter or anything like that before .map

  • Bob Jones

    Also to be aware, with generators coming/here, there may be cases where you can’t yield from a generator inside a plain-ol’-function, including a .map callback.

  • H.D. Broreau

    The usage of map in the “Confused” code example is highly confusing. I don’t know if this is done on purpose but since it is not mentioned at all in the surrounding text I assume it is not.

    Using map and not consuming its return value should not happen and will trick a lot of beginners reading this article into using map for its side effects, which is -100% functional programming and probably the exact opposite of what the author is trying to achieve. Please change it to forEach and have people learn things in the right way, even if they are beginners.

    • Kevin

      I completely agree: the code in the “Confused” section is unarguably worse than either the imperative or functional approaches.

    • Kevin

      I completely agree: the code in the “Confused” section is unarguably worse than either the imperative or functional approaches.

    • http://www.earwicker.com Daniel Earwicker

      Absolutely, and I’d also add that ES2015 has added a new form of imperative for-loop that improves on forEach, and works on any iterator (arrays, generators or user-defined).

      This is a strong indication that in a mainstream language like JS, imperative loops sometimes are the only sensible option, hence this latest acknowledgement in new language support.

    • Andrew Ritter

      The return value of map() is a new array — Isn’t he consuming it in the ‘confusing’ example via the closure within the getEmailsInLowercase function?

    • http://jezenthomas.com/ Jezen Thomas

      It was intentional. I would have thought the section being called ‘Confused’ gave it away. It is something I have seen confused developers do, incorrectly. The narrative of the article is to not shoehorn a functional concept into imperative programming. It is to adopt declarative thinking instead. Changing it to `forEach` would be to suggest developers continue thinking imperatively.

      • Jens Melgaard

        Perhaps the “Confused” step should just have been skipped all together. Map is fairly easy to understand anyways… Using [].forEach wouldn’t be “imperative” as much as it would just be the incorrect use of it here…

        Using forEach where appropriate would IMO be just as declarative. It’s perhaps just more uncommon that you just wan’t to perform something for each item and not return a result, that doesn’t rule it out though.

        I think Streams/Irritable/Generators/Enumerable or whatever the languages call them these days (and especially async handling where supported), would have a nice story in here, this is where it becomes absolute obvious that we should change the way we think about performing operations on sequences.

  • quazecoatl

    With ES6’s arrow function, this is even nicer:

    var mixedEmails = [‘JOHN@ACME.COM’, ‘Mary@FooBar.com’, ‘monty@spam.eggs’];
    var validData = mixedEmails.map(email => email.toLowerCase());

    What I’m thinking is, since in this case particular example, the logic we want to apply to each email address is simple, it’s just to make it lower-case, it’s not necessary to use a function for it.
    But if the logic is more complicated, then a function might still recommended for clarity and reusability.

    • Scott Ashmore

      This is the approach I would take but using the function keyword instead. Is it not considered bad to use an arrow function when it’s not necessary?

      • quazecoatl

        Huh, why is the ES6 arrow function a “mistake”? It’s a great new JavaScript feature, it’s not a mistake :)

        • Scott Ashmore

          I didn’t say it was a mistake, I simply asked if it was bad to use it.

          • quazecoatl

            It’s not bad at all to use it, next versions of Javascript, ES6 and ES7 have awesome features.
            Go check it out!

      • Baz

        It’s the opposite, you should always use the arrow function except when you can’t, ie need a new scope. This is because the arrow function is simpler with less possibilities and therefore easier to reason about.

        • Scott Ashmore

          Ah ok, thanks Baz!

  • Bogdan Pascanu

    Or even simpler, you can do this in two lines of code. No need to declare extra functions in this case.

    var mixedEmails = [‘JOHN@ACME.COM’, ‘Mary@FooBar.com’, ‘monty@spam.eggs’];
    mixedEmails.map(String.toLowerCase);

  • http://think.dolhub.com Lawrence Dol

    Except that with your loop you iterated the array once and with your array functions you iterated it four times. Won’t often matter, but it might. Just something to be aware of.

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in JavaScript, once a week, for free.