DEV Community

loading...

Using Python range() in JavaScript

Guy Ariely
Web developer and Computer Science student
・3 min read

What is the Python range() type?

If you’re not familiar with Python, range() refers to the use of the range type to create an immutable sequence of numbers.

“The range type represents an immutable sequence of numbers and is commonly used for looping a specific number of times in for loops.” — docs.python.org

The range() constructor has two forms of definition:

range(stop)
range(start, stop[, step])
Enter fullscreen mode Exit fullscreen mode

A concise explanation of the parameters, return value, etc. can be found on programiz.

A few examples:

Building the range() function in JavaScript

For simplicity sake, we will ignore the optional step argument.

By using the Array constructor, fill and map, you could work out a simple solution in a quick one-liner:

new Array(stop - start).fill(start).map((el, i) => el + i)
Enter fullscreen mode Exit fullscreen mode

And maybe then offer a more complete solution, covering the case of calling range with only one argument:

But it’s not quite it yet. Can you see why this solution is wrong?

Remember, calling Python range returns an immutable sequence of numbers. Notice how in order to get the familiar list data structure, the Python examples above wrap the return value of range with list().

An equal example in JavaScript should probably look something like this:

> Array.from(range(1, 11))
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Enter fullscreen mode Exit fullscreen mode

So how can we make a function in JavaScript return an “immutable sequence of numbers”? Is there any way to achieve the same structure of the range() return value in JavaScript?

Iterators to the rescue!

Iterators are wildly used in different programming languages to allow us to iterate over different data structures.

“In JavaScript an iterator is an object which defines a sequence and potentially a return value upon its termination.” —developer.mozilla.org

Specifically in JavaScript, an iterator is any object which implements the Iterator protocol by having a next() method that returns an object with two properties:

next() {
  ...
  return {
    value: // current value to be passed
    done: // did we finish iterating over the data structure?
  }
}
Enter fullscreen mode Exit fullscreen mode

Using an iterator, you can provide your own logic on how to iterate. For example, here is a simple iterator that will skip every second item:

More importantly, If we create an object that defines [Symbol.iterator] which returns that iterator, we can get exactly the behavior we were looking for:

Play around with these examples and see what kind of interesting and useful iterators you can create 💪.

By now you can probably imagine how we might approach creating Python range() in JavaScript. This is how I implemented it:

As I mentioned, for simplicity's sake this implementation leaves out the step argument from the original Python range(), but it’s just a matter of extra logic that you can implement yourself. Feel free to @ me your solution ✌️.

Discussion (10)

Collapse
lionelrowe profile image
lionel-rowe • Edited

You can significantly simplify by using a generator function, something like this:

const range = (start, stop) => {
    if (stop === undefined) {
        stop = start
        start = 0
    }

    return {
        *[Symbol.iterator]() {
            for (let n = start; n < stop; ++n) yield n
        },
    }
}
Enter fullscreen mode Exit fullscreen mode

You can also write custom functions for map, filter, reduce, etc. that will operate lazily on iterables:

const map = (fn) => (iter) => ({
        *[Symbol.iterator]() {
            for (const x of iter) yield fn(x)
        },
    })

const filter = (fn) => (iter) => ({
        *[Symbol.iterator]() {
            for (const x of iter) {
                if (fn(x)) yield x
            }
        },
    })

const reduce = (init, fn) => (iter) => ({
        *[Symbol.iterator]() {
            let acc = init

            for (const cur of iter) {
                yield (acc = fn(acc, cur))
            }
        },
    })
Enter fullscreen mode Exit fullscreen mode

Laziness in action:

const log = x => (console.log(x), x)

for (const x of map(log)(range(Number.MAX_SAFE_INTEGER))) {
    if (x >= 3) break // only logs 0, 1, 2, 3
}
Enter fullscreen mode Exit fullscreen mode

If you tried to do that with an array, you'd run out of memory.

Collapse
jcubic profile image
Jakub T. Jankiewicz • Edited

Your code is really weird you mix iterator protocol with generators. You just can use this, it will do the same:

function* range(start, stop) {
    if (stop === undefined) {
        stop = start
        start = 0
    }
    for (let n = start; n < stop; ++n) yield n;
}
Enter fullscreen mode Exit fullscreen mode

I've also written an article on higer order iterators including async but in Polish (you can use translate option to read it).

Generatory i Iteratory wyższego poziomu

Collapse
lionelrowe profile image
lionel-rowe

Not weird at all, it's a common pattern. That way, you can easily extend your range objects with other properties, like min, max, includes, etc.

More importantly, it also ensures you don't accidentally mutate the range and get unexpected results. With your version:

const r = range(1, 10)
;[...r] // [1, 2, 3, 4, 5, 6, 7, 8, 9], as expected
;[...r] // [], empty array, because the iterator has already been consumed
Enter fullscreen mode Exit fullscreen mode

With my version:

const r = range(1, 10)
;[...r] // [1, 2, 3, 4, 5, 6, 7, 8, 9], as expected
;[...r] // [1, 2, 3, 4, 5, 6, 7, 8, 9], same as before
Enter fullscreen mode Exit fullscreen mode

You can still get a mutable iterator from my version if you really need one, though. You just have to ask for it explicitly:

const i = range(1, 10)[Symbol.iterator]()
;[...i] // [1, 2, 3, 4, 5, 6, 7, 8, 9]
;[...i] // []
Enter fullscreen mode Exit fullscreen mode
Collapse
guyariely profile image
Guy Ariely Author

Hey lionel, thanks for the great feedback.
The idea of using generators looks really interesting. I'll look into it and maybe do a follow up article on Generators.

Collapse
lionelrowe profile image
lionel-rowe

What you get with a generator function is basically what you hand-rolled with return { value, done }:

const gen = (function*() { yield 1 })()
// Object [Generator] {}
gen.next()
// { value: 1, done: false }
gen.next()
// { value: undefined, done: true }
Enter fullscreen mode Exit fullscreen mode

They're pretty nifty 😊

Collapse
dabat profile image
Daniel Battaglia

Hey, thanks for sharing this @guyariely very timely, as I was just looking at JavaScript range and generator functions 🙂

One question I have is what is the advantage of using an iterator here, rather than a simple for n to n and returning and array?

I found this article (joshwcomeau.com/snippets/javascrip...) which uses a for n to generate the output, and seems to work just as well, and also, like @lukeshir mentioned, supports forEach because the return is an array.

const range = (start, end, step = 1) => {
  let output = [];
  if (typeof end === 'undefined') {
    end = start;
    start = 0;
  }
  for (let i = start; i < end; i += step) {
    output.push(i);
  }
  return output;
};
Enter fullscreen mode Exit fullscreen mode

Link to a sandbox comparison: codesandbox.io/s/range-4idy5?file=...

Anyhow, thanks again for sharing!

Collapse
guyariely profile image
Guy Ariely Author

Hey Daniel, The reason I used iterators was to match Python range structure.
Notice how in order to get the python list (or Array in JavaScript), you have to call list(). The same kind of structure and semantics can be achieved in JavaScript by using iterators.

I agree this isn't the greatest use case for iterators, but it's simple and opens up the door for further exploration of this great tool.

Collapse
lukeshiru profile image
LUKESHIRU

Interesting! One thing might worth mentioning is that this will work with for, but not so much with Array.prototype.map, or at least not directly ... you need to do:

[...range(0, 10)];
Enter fullscreen mode Exit fullscreen mode

Which returns an array that can be mutated. Still, being just numbers, you could just have something like your first example, and it would be more than ok:

const range = (start = 0, stop = [start, (start = 0)][0], step = 1) =>
    (step > 0 ? start < stop : start > stop)
        ? [...Array(Math.abs(Math.ceil((stop - start) / step))).keys()].map(
                index => start + step * index
          )
        : [];

// And then you can just use for ...
for (const item of range(3, 7)) {
  console.log(item);
}

// But also a forEach if you want:
range(3, 7).forEach(item => console.log(item));
Enter fullscreen mode Exit fullscreen mode

Cheers!

Collapse
guyariely profile image
Guy Ariely Author

hey luke thanks for reading the article!
The reason I chose to use iterators is to match Python range() structure and semantics. I was looking to build a function / type that will only produce an array after calling Array.from() on the returned structure, similar to python list().

Collapse
ryands17 profile image
Ryan Dsouza

What's the difference b/w the above range function as opposed to this?

function* range(start, stop, step = 1) {
  if (stop === undefined) {
    stop = start
    start = 0
  }

  for (let i = start; i < stop; i += step) {
    yield i
  }
}
Enter fullscreen mode Exit fullscreen mode