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.
1 Answer 1
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});
})();
-
\$\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– Joseph Silber2018年03月25日 02:16:28 +00:00Commented Mar 25, 2018 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– Joseph Silber2018年03月25日 02:16:50 +00:00Commented Mar 25, 2018 at 2:16
Explore related questions
See similar questions with these tags.
cons
itself ((:)
in Haskell). Though can be implemented with a function each time, it probably makes sense to make it specific. \$\endgroup\$return new List(function* ()
code can be reduced. One could probably dynamically definefilter
,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\$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 definefilter
,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\$