HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavascriptMinor

Implementing range() in JavaScript

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
javascriptrangeimplementing

Problem

For fun, I implemented a version of Python's range function in JavaScript. I found that underscore.js implements it as well, but with notable differences. I'm hoping others can comment on the differences. I assume that underscore's implementation is, if not more optimal, at least more idiomatic. And I'd like to build my intuition for the best way to do things in JavaScript.

My version:

var range = function () {
    var results = [];

    // Allow comparison to be variable
    var lessThan = function (a, b) {return (a  b); };

    if (arguments.length === 1) {
        var start = 0;
        var stop = arguments[0];
        var step = 1;
        var comparisonTest = lessThan;
    } else {
        var start = arguments[0];
        var stop = arguments[1];
        var step = arguments[2] || 1;
        var comparisonTest = ((step > 0)? lessThan : greaterThan);
    };

    for (var i = start; comparisonTest(i, stop) ; i += step) {
        results.push(i);
    };

    return results;
};


And underscore's implementation:

_.range = function(start, stop, step) {
    if (arguments.length <= 1) {
      stop = start || 0;
      start = 0;
    }
    step = arguments[2] || 1;

    var length = Math.max(Math.ceil((stop - start) / step), 0);
    var idx = 0;
    var range = new Array(length);

    while(idx < length) {
      range[idx++] = start;
      start += step;
    }

    return range;
  };


I see that not specifying function parameters - which made it easier to deal with variable lengths of arguments, ended up being more verbose than the way underscore handles it. Is there any other reason to always specify parameters?

And how about the actual building up of the range - underscore declares an Array constructor with length. Is that necessary?

Any insight or comments is appreciated!

Solution

I see that not specifying function parameters - which made it easier to deal with variable lengths of arguments

But it didn't really make it easier to deal with variable length args, as underscore's code demonstrates. It's true, though, that it makes it a lot easier to write the function definition without worrying about whether you should write function (start), function (start, end), or function (start, end, step)- but that's about it.

But regardless of how you write it in code document it.

Since JS doesn't really care too much about what you put in the args list, docs are the what you have to rely on.

Anyway, you can basically put whatever you want in the args list. But I'd lean toward following underscore's example, and write out the full list of arguments. Worst case scenario is that someone skips the docs, looks at the args list, and sees 3 arguments. So he/she writes calls the function with all of those argument, even though 1 or 2 would have achieved the same thing. But that's just fine. But if you don't write anything in the args list and don't provide docs, whomever wants to use your code (e.g. you yourself in 2 months) suddenly has to play JavaScript interpreter and read the code itself to figure it out. While that shouldn't take more than a couple of seconds it's still an annoyance.

Alternatively, you can do something like

function (start /, [stop, [end]] /)

to clue a developer in on what the function expects. But that's still not a substitute for just documenting it well. Because even if you are looking at the function's code itself and figuring it out, you have no idea what arguments[1] might be, because there's nothing to indicate there even is a 2nd argument. You have to work backwards to understand it. Underscore has named arguments, that, even if they're undefined, just make the code itself a lot more readable right off the bat.


underscore declares an Array literal with length. Is that necessary?

Necessary? No. A good idea? Yes.

By declaring the length right away, you save the computer the trouble of keeping up with a growing array. When you declare the length, the computer can start allocating the necessary memory right away, whereas if you build it up, it may have to allocate more memory for each push(), and copy chunks of memory around and similar busywork.

Will you ever notice the difference? Depends on what you're doing, but I'd say wager that no, you won't ever notice. But it's a pretty simple optimization to make, so why not? Not that this is something you have to always do when building arrays (and sometimes you just can't know ahead of time how big the array will get), but for a low-level function like this with potentially heavy usage? Yeah, might as well. It's still very readable, so it's a win-win, really.

Next up: Variables. Just declare your variables in one place - with sensible default values, if possible. You declare the same 4 vars in two different places. While it doesn't matter much, it's just basic code cleanliness to not repeat yourself.

Next up: The loop. You loop basically does this:

  • declare and initialize i



  • call comparisonTest with a couple of arguments



  • compare those arguments



  • return boolean



  • if returned value is false, exit the loop



  • if returned value is true:



  • append i to the array



  • increment i by step



  • repeat



Whereas underscore's does this:

  • compare idx and length



  • if false, exit the loop



  • if true:



  • insert start at index idx



  • increment idx



  • increment start by step



  • repeat



Your way seems to save a variable (underscore has both idx and start), but it's a losing battle: underscore's way saves an entire function call for every iteration of the loop.

Calling that comparison function means duplicating 2 variables (the arguments) plus the whole rigamarole of setting up return address, switching to the function's context, setting up caller and callee, arguments and all the other stuff a JS runtime does when you shove something onto the stack. Then it does the same sort of thing again when calling push. Since push is a native function, the calling overhead is no doubt lower, but there's still the aforementioned overhead of (potentially) having to reallocate memory for a bigger array and all that.

Oh, and the engine also has to create those comparison functions when you call range() (I'd wager this is also pretty well optimized, but worst case scenario is a lot of extra work on every call just to do a simple comparison)

It's naturally more efficient to just not do any of that, as in underscore's solution.

Now, let me just make it clear that you rarely need to care about all of this. Once you start micro-optimizing your code, it's a deep, deep rabbit hole to go down, and most of the time it really doesn't matter. You're just spinning your wheels. All of this stuff is worth being aware of, but don't go overboard. "Premature optimization is the root of all evil

Context

StackExchange Code Review Q#52442, answer score: 3

Revisions (0)

No revisions yet.