4
\$\begingroup\$

I built this very-basic lazy list (I'll add more methods as I need them). You provide it an array, a generator or any iterator. It creates a lazy list, which lets you run a pipeline of transformations in a lazy manner, meaning that they'll only be applied as you pull values out of the list.

Here's the class:

class List {
    static range(start, end) {
        return new List(function* () {
            while (start <= end) {
                yield start++;
            }
        });
    }

    constructor(source) {
        if (typeof source == 'function') {
            this.generator = source;
        } else {
            this.generator = function* () {
                yield* source;
            };
        }
    }

    filter(predicate) {
        return new List(function* () {
            for (const item of this) {
                if (predicate(item)) {
                    yield item;
                }
            }
        }.bind(this));
    }

    map(mapper) {
        return new List(function* () {
            for (const item of this) {
                yield mapper(item);
            }
        }.bind(this));
    }

    take(size) {
        return new List(function* () {
            for (const item of this) {
                if (size--) {
                    yield item;
                } else {
                    break;
                }
            }
        }.bind(this));
    }

    *[Symbol.iterator] () {
        yield* this.generator();
    }

    toArray() {
        return [...this];
    }
}

Here's a simple example of how to use it:

List.range(1, 10 ** 9)
    .filter(number => number % 10 == 0)
    .map(number => 'Item ' + number)
    .take(5)
    .toArray();

Trying the same with a regular array:

Array.from({ length: 10 ** 9 }, (v, i) => i + 1)
     .filter(number => number % 10 == 0)
     .map(number => 'Item ' + number)
     .slice(0, 5);

...will run out of memory before it completes.

\$\endgroup\$
  • 1
    \$\begingroup\$ Sidenote: not having generator arrow functions saddens me. \$\endgroup\$ – Joseph Silber Mar 23 '18 at 12:36
  • \$\begingroup\$ Currently it lacks the very basic list constructor, the cons itself ((:) in Haskell). Though can be implemented with a function each time, it probably makes sense to make it specific. \$\endgroup\$ – bipll Mar 23 '18 at 12:42
  • \$\begingroup\$ I am curious to see how the reoccurring return new List(function* () code can be reduced. One could probably dynamically define filter, map, take from an array of the inner generator functions. Not sure if this is clean, though, and one loses all the typings (when using TypeScript). \$\endgroup\$ – ComFreek Mar 23 '18 at 12:48
  • \$\begingroup\$ @ComFreek - I don't think that's possible, since you must yield, and the only way to do that is within a generator function. Also, I'm not sure what you mean by "One could probably dynamically define filter, map, take from an array of the inner generator functions". There's no array, which is the whole point here. The values are all flowing down one by one. \$\endgroup\$ – Joseph Silber Mar 23 '18 at 12:50
  • \$\begingroup\$ See pastebin.com/70nPzuzp. \$\endgroup\$ – ComFreek Mar 23 '18 at 12:59
2
\$\begingroup\$

I can not find fault with your code apart from the use of class of which i am not a fan.

So I will just present an alternative syntax that gives a little extra encapsulated protection. Its usage is slightly different. List is instantiated via a factory and closure holds the generator list. I also freeze each instance of List to further protect the state.

It also does not have to mess about with const list = this which from your side-note comment is a bit of an annoyance.

const lazyList = (() => {

    const range = (start, end) => List(function* () {
       while(start <= end) { yield start++ }
    });    
    function List(source) {
        var list;
        if (typeof source === "function") { list = source }
        else { list = function* () { yield* source } }

        return Object.freeze({
            filter(pred){
                return List(function* () {
                    for (const item of list()) { pred(item) ? yield item : 0 }
                });
            },
            map(map) {
                return List(function* () {
                    for (const item of list()) { yield map(item) }
                });
            },
            take(count) {
                return List(function* () {
                    for (const item of list()) {
                        if (count--) { yield item }
                        else { break }
                    }
                });
            },
            toArray() { return [...list()] },
            *[Symbol.iterator] () { yield* list() }
        });
    } 
    return Object.freeze({range, List});

})();
\$\endgroup\$
  • \$\begingroup\$ I wouldn't call using classes a "fault". It's ok for you to prefer a more functional, closure-based approach, but I vastly prefer sticking all my methods on the prototype so that I don't have to recreate them for every single list created. \$\endgroup\$ – Joseph Silber Mar 25 '18 at 2:16
  • \$\begingroup\$ Also, I got rid of all the const list = this by binding the functions to the current object. \$\endgroup\$ – Joseph Silber Mar 25 '18 at 2:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.