DEV Community

Discussion on: Using Python range() in JavaScript

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

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 😊