Reputation: 214949
I've got this recursive generator
var obj = [1,2,3,[4,5,[6,7,8],9],10]
function *flat(x) {
if (Array.isArray(x))
for (let y of x)
yield *flat(y)
else
yield 'foo' + x;
}
console.log([...flat(obj)])
It works fine, but I don't like the for
part. Is there a way to write it functionally? I tried
if (Array.isArray(x))
yield *x.map(flat)
which didn't work.
Is there a way to write the above function without for
loops?
Upvotes: 14
Views: 5906
Reputation: 664297
Is there a way to write it functionally, without
for
loops?
No, not really. (Of course you can always opt for recursion instead, but I'll question the usefulness of that approach).
What we're looking for are functional combinators for iterators:
function* of(x) { // also known as `pure` or `return`
yield x;
}
function map(f) { return function* (xs) { // also known as `fmap`
for (const x of xs)
yield f(x);
}
function* join(xss) { // also known as `concat` (not `append`!) or `flatten` (but non-recursive!)
for (const xs of xss)
for (const x of xs)
yield x;
}
function chain(f) { return function* (xs) { // also known as `concatMap` or `bind`
for (const x of xs)
const ys = f(x);
for (const y of ys)
yield y;
}
// or const chain = f => compose(concat, map(f)) :-)
Now we can just treat iterators as a monad, and be no more concerned about the implementation.
As you can see, I have not used the syntax yield* xs
above which is (basically) just sugar for
for (const x of xs)
yield x;
What is looking weird in your implementation is the disparity between the outer loop and the inner non-loop. In an optimal world, there would be a yield**
syntax that did what join
does, but there's not. So we can only implement your function nicely with the above helper functions:
function* flat(x) {
if (Array.isArray(x))
yield* chain(flat)(x);
else
yield* of('foo' + x); // foreshadowing
}
or just
function flat(x) {
return Array.isArray(x) ? chain(flat)(x) : of('foo' + x);
}
Upvotes: 2
Reputation: 386550
You could use rest parameters ...
and check the length of the rest array for another calling of the generator
function* flat(a, ...r) {
if (Array.isArray(a)) {
yield* flat(...a);
} else {
yield 'foo' + a;
}
if (r.length) {
yield* flat(...r);
}
}
var obj = [1, 2, 3, [4, 5, [6, 7, 8], 9], 10];
console.log([...flat(obj)])
.as-console-wrapper { max-height: 100% !important; top: 0; }
A similar approach but with a spread
generator for calling the handed over generator with the spreaded values.
function* spread(g, a, ...r) {
yield* g(a);
if (r.length) {
yield* spread(g, ...r);
}
}
function* flat(a) {
if (Array.isArray(a)) {
yield* spread(flat, ...a);
} else {
yield 'foo' + a;
}
}
var obj = [1, 2, 3, [4, 5, [6, 7, 8], 9], 10];
console.log([...flat(obj)])
.as-console-wrapper { max-height: 100% !important; top: 0; }
Upvotes: 3
Reputation: 350137
The map
is a good idea, but you need to reduce the resulting array of generator objects to just one generator object:
function *flat(x) {
if (Array.isArray(x))
yield *x.map(flat).reduce((a, b) => function*() { yield *a; yield *b }());
else
yield 'foo' + x;
}
var obj = [1,2,3,[4,5,[6,7,8],9],10];
console.log([...flat(obj)]);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Upvotes: 2
Reputation: 332
may be something like
var obj = [1, 2, 3, [4, 5, [6, 7, 8], 9], 10];
function* flat(x) {
if (Array.isArray(x)) {
yield x.map(v => {
return [...flat(v)].join();
});
} else yield "foo" + x;
}
console.log([...flat(obj)]);
Upvotes: -1
Reputation: 45121
You could reduce array to generator. But this looks worse than for loop to me (though is functional :) )
var obj = [1, 2, 3, [4, 5, [6, 7, 8], 9], 10]
function* flat(x) {
if (Array.isArray(x))
yield * x.reduceRight(
(f, y) => function*() {
yield * flat(y);
yield * f()
},
function*() {}
)()
else
yield 'foo' + x;
}
console.log([...flat(obj)])
Upvotes: 1