I have a generator called generateNumbers
in JavaScript and another generator generateLargerNumbers
which takes each value generated by generateNumbers
and applies a function addOne
to it, as such:
function addOne(value) {
return value + 1
}
function* generateNumbers() {
yield 1
yield 2
yield 3
}
function* generateLargerNumbers() {
for (const number of generateNumbers()) {
yield addOne(number)
}
}
Is there any terser way to do this without building an array out of the generated values? I'm thinking something like:
function* generateLargerNumbers() {
yield* generateNumbers().map(addOne) // obviously doesn't work
}
Generator functions are written using the function* syntax. When called, generator functions do not initially execute their code. Instead, they return a special type of iterator, called a Generator.
A generator is a process that can be paused and resumed and can yield multiple values. A generator in JavaScript consists of a generator function, which returns an iterable Generator object.
The map() method in JavaScript creates an array by calling a specific function on each element present in the parent array. It is a non-mutating method. Generally map() method is used to iterate over an array and calling function on every element of array.
The function* declaration ( function keyword followed by an asterisk) defines a generator function, which returns a Generator object.
higher-order generators
You can choose to manipulate the generator functions themselves
const Generator =
{
map: (f,g) => function* (...args)
{
for (const x of g (...args))
yield f (x)
},
filter: (f,g) => function* (...args)
{
for (const x of g (...args))
if (f (x))
yield x
}
}
// some functions !
const square = x =>
x * x
const isEven = x =>
(x & 1) === 0
// a generator !
const range = function* (x = 0, y = 1)
{
while (x < y)
yield x++
}
// higher order generator !
for (const x of Generator.map (square, Generator.filter (isEven, range)) (0,10))
console.log('evens squared', x)
higher-order iterators
Or you can choose to manipulate iterators
const Iterator =
{
map: (f, it) => function* ()
{
for (const x of it)
yield f (x)
} (),
filter: (f, it) => function* ()
{
for (const x of it)
if (f (x))
yield x
} ()
}
// some functions !
const square = x =>
x * x
const isEven = x =>
(x & 1) === 0
// a generator !
const range = function* (x = 0, y = 1)
{
while (x < y)
yield x++
}
// higher-order iterators !
for (const x of Iterator.map (square, Iterator.filter (isEven, range (0, 10))))
console.log('evens squared', x)
recommendation
In most cases, I think it's more practical to manipulate the iterator because of it's well-defined (albeit kludgy) interface. It allows you to do something like
Iterator.map (square, Iterator.filter (isEven, [10,11,12,13]))
Whereas the other approach is
Generator.map (square, Generator.filter (isEven, Array.from)) ([10,11,12,13])
Both have a use-case, but I find the former much nicer than the latter
persistent iterators
JavaScript's stateful iterators annoy me – each subsequent call to .next
alters the internal state irreversibly.
But! there's nothing stopping you from making your own iterators tho and then creating an adapter to plug into JavaScript's stack-safe generator mechanism
If this interests you, you might like some of the other accompanying examples found here: Loop to a filesystem structure in my object to get all the files
The only gain isn't that we can reuse a persistent iterator, it's that with this implementation, subsequent reads are even faster than the first because of memoisation – score: JavaScript 0, Persistent Iterators 2
// -------------------------------------------------------------------
const Memo = (f, memo) => () =>
memo === undefined
? (memo = f (), memo)
: memo
// -------------------------------------------------------------------
const Yield = (value, next = Return) =>
({ done: false, value, next: Memo (next) })
const Return = value =>
({ done: true, value })
// -------------------------------------------------------------------
const MappedIterator = (f, it = Return ()) =>
it.done
? Return ()
: Yield (f (it.value), () => MappedIterator (f, it.next ()))
const FilteredIterator = (f, it = Return ()) =>
it.done
? Return ()
: f (it.value)
? Yield (it.value, () => FilteredIterator (f, it.next ()))
: FilteredIterator (f, it.next ())
// -------------------------------------------------------------------
const Generator = function* (it = Return ())
{
while (it.done === false)
(yield it.value, it = it.next ())
return it.value
}
// -------------------------------------------------------------------
const Range = (x = 0, y = 1) =>
x < y
? Yield (x, () => Range (x + 1, y))
: Return ()
const square = x =>
x * x
const isEven = x =>
(x & 1) === 0
// -------------------------------------------------------------------
for (const x of Generator (MappedIterator (square, FilteredIterator (isEven, Range (0,10)))))
console.log ('evens squared', x)
There isn't a built-in way to map over Generator
objects, but you could roll your own function:
const Generator = Object.getPrototypeOf(function* () {});
Generator.prototype.map = function* (mapper, thisArg) {
for (const val of this) {
yield mapper.call(thisArg, val);
}
};
Now you can do:
function generateLargerNumbers() {
return generateNumbers().map(addOne);
}
const Generator = Object.getPrototypeOf(function* () {});
Generator.prototype.map = function* (mapper, thisArg) {
for (const val of this) {
yield mapper.call(thisArg, val);
}
};
function addOne(value) {
return value + 1
}
function* generateNumbers() {
yield 1
yield 2
yield 3
}
function generateLargerNumbers() {
return generateNumbers().map(addOne)
}
console.log(...generateLargerNumbers())
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With