For example, I need to cache last task outside of the loop (redux-saga).
export default function* () {
let lastTask;
while (true) {
const action = yield take(someActions);
// some conditions there
if (lastTask) {
yield cancel(lastTask);
}
lastTask = yield fork(send(action));
}
}
What pattern should I use there to make it in a functional immutable way without let?
One option is to have the generator recursively yield* itself:
function* gen (lastTask) {
const action = yield take(someActions);
// some conditions there
if (lastTask) {
yield cancel(lastTask);
}
const nextLastTask = yield fork(send(action));
yield* gen(nextLastTask);
}
export default gen;
But note that nothing in your original code (or this code) involves mutation - the problem with let is that it permits reassignment, which is somewhat similar, but not the same thing. (Best to avoid mutation and reassignment, if possible, as long as the code remains easy to read)
Make sure the consumer calls gen with no arguments, of course.
Related
I see most of the examples in redux-saga using while(true){}:
function* watcherSaga(){
while (true) {
yield something()
}
}
Can we not simply write?
function* watcherSaga(){
yield something()
}
Or, is there anything difference?
Take a look at the following example. One generator is never "done" and the other generator is done after the first (and only) yield.
function something() {
return Math.random();
}
function* watcherSaga1() {
while (true) {
yield something();
}
}
function* watcherSaga2() {
yield something();
}
const watcher1 = watcherSaga1();
const watcher2 = watcherSaga2();
console.log('watcher1: ', watcher1.next());
console.log('watcher1: ', watcher1.next());
console.log('watcher1: ', watcher1.next());
console.log('watcher2: ', watcher2.next());
console.log('watcher2: ', watcher2.next());
console.log('watcher2: ', watcher2.next());
With the while loop, the generator will continue to yield values forever. Without, it's just once.
When you have a need for some sequence (say, multiples of 7), a function like that can easily supply such a sequence to a consumer that imposes its own limits on how many values it needs.
Generators provide an extremely powerful way of structuring code, and have a way of permeating a design in some cases. They're particularly useful in the context of media generation, like p5.js, where there's lots of interacting iteration. Generators provide a particularly nice way to encapsulate that.
Generators returns an Iterator.
When you call next() on the iterator, it produces the return value and if the function comes to end, it will generate an undefined value and gets done.
Check the below snippet. Hope this one helps you.
function* watcherSaga() {
var i = 0;
while (true) {
yield i++;
}
}
const sagaIterator = watcherSaga();
console.log("**** with while() loop *****");
console.log(sagaIterator.next());
console.log(sagaIterator.next());
console.log(sagaIterator.next());
// You can keep calling sagaIterator.next() and it never gets "done" because of "while(true)"
function* watcherSagaWithoutWhile() {
var i = 0;
yield i++;
}
const sagaIteratorWİthoutWhite = watcherSagaWithoutWhile();
console.log("**** withOUT while() loop *****");
console.log(sagaIteratorWİthoutWhite.next());
console.log(sagaIteratorWİthoutWhite.next());
console.log(sagaIteratorWİthoutWhite.next());
// The second call to "next()" will return an "undefined" value and the generator gets done because the generator comes to the end
For further reading: https://basarat.gitbooks.io/typescript/docs/generators.html
I am trying to use a clean arrow function that calls yield fork(....). I have a lot of places this happens. I am trying to do this:
const { ids } = yield take('REQUEST');
// ids is array of numbers [1, 2, 3]
ids.forEach(id => yield fork(requestWorker, id)); ////// this is what im trying to do - this is pseudo-code i know you cant use in non-generators (non-super-star functions)
However this does not work and i am having to do:
for (const id of ids) {
yield fork(requestWorker, id);
}
instead of the:
ids.forEach(id => yield fork(requestWorker, id)); // this is pseudo-code i know you cant use in non-generators (non-super-star functions)
Is there a cleaner way then a for-of loop? Like this arrow function method?
You could yield another generator
function* take(x) {
var id = /* ... */,
requesWroker = /* ... */;
yield* fork(requesWroker, id);
}
var all = [...take('REQUEST')];
On Mozilla's page iterators and generators there is a statement:
While custom iterators are a useful tool, their creation requires
careful programming due to the need to explicitly maintain their
internal state. Generators provide a powerful alternative: they allow
you to define an iterative algorithm by writing a single function
which can maintain its own state.
Regarding above explanation, isn't it possible to write an iterative algorithm without Generators, such as:
Array[Symbol.iterator] = function(){
return {
next: function(){
//logic
return {
value: "",
done:false
}
}
}
}
Can't get my head around. Could someone explain what is the main reason they created an alternative, seems not much different to me.
They might look pretty similar on the surface, but they can be used in very different ways.
Iterators and Iterables
Iterators are rather strictly defined: they are object (the iterators) which contains a next (and possibly a few other) function. Every time the next function is called, it is expected to return an object with two properties:
value: the current value of the iterator
done: is the iterator finished?
An iterable on the other hand is an object which has a property with a Symbol.iterator key (which represents the well know symbol ##iterator). That key contains a function, which when called, returns a new iterator.
An example of an iterable:
const list = {
entries: { 0: 'a', 1: 'b' },
[Symbol.iterator]: function(){
let counter = 0;
const entries = this.entries;
return {
next: function(){
return {
value: entries[counter],
done: !entries.hasOwnProperty(counter++)
}
}
}
}
};
Their main purpose, as their name suggests, is to provide an interface which can be iterated:
for (let item of list) { console.log(item); }
// 'a'
// 'b'
Generators
Generators on the other hand are much more versatile. It helps to think of them as functions which can be paused and resumed.
While they can be iterated (their iterables provide a next method), they can implement much more sophisticated procedures and provide a input/output communication through their next method.
A simple generator:
function *mygen () {
var myVal = yield 12;
return myVal * 2;
}
const myIt = mygen();
const firstGenValue = myIt.next().value;
// Generator is paused and yields the first value
const result = myIt.next(firstGenValue * 2).value;
console.log(result); // 48
Generator delegation
Generators can delegate to another generator:
function *mydelgen(val) {
yield val * 2;
}
function *mygen () {
var myVal = yield 12;
yield* mydelgen(myVal); // delegate to another generator
}
const myIt = mygen();
const val = myIt.next().value;
console.log(val);
console.log(myIt.next(val).value);
console.log(myIt.next().value);
Generators & Promises
Generators and Promises together can create a sort of automatic asynchronous iterator with the help of utilities such as co.
co(function *(){
// resolve multiple promises in parallel
var a = Promise.resolve(1);
var b = Promise.resolve(2);
var c = Promise.resolve(3);
var res = yield [a, b, c];
console.log(res);
// => [1, 2, 3]
}).catch(onerror);
In Conclusion
So in conclusion one could say that the main purpose of iterators is to create an interface for custom objects to be iterated, whereas generators provide a plethora of possibilities for synchronous and asynchronous workflows:
stateful functions
generator delegation
generators & promises
CSP
etc.
Isn't it possible to write an iterative algorithm without Generators.
No, it's not. Yes, it is possible to write every generator algorithm as a custom iterator, but the // logic in your code will be much more complicated. The emphasis of the statement is that it won't be iterative any more, it will be recursive.
As an exercise, here's a quite simple iterative generator function:
function* traverseTree(node) {
if (node == null) return;
yield* traverseTree(node.left);
yield node.value;
yield* traverseTree(node.right);
}
Try to rewrite it as a custom iterator. Whether you get stuck or get it done, it will show you what the difference is.
var A = {
demo : function() * {
/* Some logic here, but no yield is used */
}
}
What is the use of a generator method that does not yield anything?
Have you ever used something like this? What was the use case?
It's quite the same case like an empty function - someone wants to call a function, but you have nothing to do.
Similarly, an empty generator function is a function which creates a generator that does nothing. It does represent the empty sequence. However, a generator function that doesn't yield isn't necessarily empty - it can still do something and have a result value, but there simply are no intermediate results.
The following code prints 'someValue' on the response every 100 ms for 5 seconds. It does not use yield.
const Koa = require('koa');
const through = require('through');
(new Koa()).use(function *(){
const tr = through();
setInterval(() => tr.write('someValue\n'), 100);
setTimeout(tr.end, 5000);
this.body = tr;
}).listen(3003, () => {});
Access with: curl localhost:3003
Learn Generators - 4 » CATCH ERROR!
The solution uses a for loop but I just couldn't find anything in MDN - Iteration Protocols that refers to yield within callbacks.
I'm going to guess the answer is just don't do that but thanks in advance if anyone has the time or inclination to provide an explanation!
Code:
function *upper (items) {
items.map(function (item) {
try {
yield item.toUpperCase()
} catch (e) {
yield 'null'
}
}
}
var badItems = ['a', 'B', 1, 'c']
for (var item of upper(badItems)) {
console.log(item)
}
// want to log: A, B, null, C
Error:
⇒ learn-generators run catch-error-map.js
/Users/gyaresu/programming/projects/nodeschool/learn-generators/catch-error-map.js:4
yield item.toUpperCase() // error below
^^^^
SyntaxError: Unexpected identifier
at exports.runInThisContext (vm.js:73:16)
at Module._compile (module.js:443:25)
at Object.Module._extensions..js (module.js:478:10)
at Module.load (module.js:355:32)
at Function.Module._load (module.js:310:12)
at Function.Module.runMain (module.js:501:10)
at startup (node.js:129:16)
at node.js:814:3
Even my editor knows this is a terrible idea...
Disclaimer: I'm the author of Learn generators workshopper.
Answer by #slebetman is kinda correct and I also can add more:
Yes, MDN - Iteration Protocol doesn't refer directly about yield within callbacks.
But, it tell us about importance from where you yield item, because you can only use yield inside generators. See MDN - Iterables docs to find out more.
#marocchino suggest just fine solution iterate over Array that was changed after map:
function *upper (items) {
yield* items.map(function (item) {
try {
return item.toUpperCase();
} catch (e) {
return null;
}
});
}
We can do it, because Array has iteration mechanism, see Array.prototype[##iterator]().
var bad_items = ['a', 'B', 1, 'c'];
for (let item of bad_items) {
console.log(item); // a B 1 c
}
Array.prototype.map doesn't have default iteration behavior, so we couldn't iterate over it.
But generators is not just iterators. Every generator is an iterator, but not vice versa. Generators allows you to customize iteration (and not only) process by calling yield keyword. You can play and see the difference between generators/iterators here:
Demo: babel/repl.
One problem is yield yields just one level to the function's caller. So when you yield in a callback it may not do what you think it does:
// The following yield:
function *upper (items) { // <---- does not yield here
items.map(function (item) { // <----- instead it yields here
try {
yield item.toUpperCase()
} catch (e) {
yield 'null'
}
}
}
So in the code above, you have absolutely no access to the yielded value. Array.prototype.map does have access to the yielded value. And if you were the person who wrote the code for .map() you can get that value. But since you're not the person who wrote Array.prototype.map, and since the person who wrote Array.prototype.map doesn't re-yield the yielded value, you don't get access to the yielded value(s) at all (and hopefully they will be all garbage collected).
Can we make it work?
Let's see if we can make yield work in callbacks. We can probably write a function that behaves like .map() for generators:
// WARNING: UNTESTED!
function *mapGen (arr,callback) {
for (var i=0; i<arr.length; i++) {
yield callback(arr[i])
}
}
Then you can use it like this:
mapGen(items,function (item) {
yield item.toUpperCase();
});
Or if you're brave you can extend Array.prototype:
// WARNING: UNTESTED!
Array.prototype.mapGen = function *mapGen (callback) {
for (var i=0; i<this.length; i++) {
yield callback(this[i])
}
};
We can probably call it like this:
function *upper (items) {
yield* items.mapGen(function * (item) {
try {
yield item.toUpperCase()
} catch (e) {
yield 'null'
}
})
}
Notice that you need to yield twice. That's because the inner yield returns to mapGen then mapGen will yield that value then you need to yield it in order to return that value from upper.
OK. This sort of works but not quite:
var u = upper(['aaa','bbb','ccc']);
console.log(u.next().value); // returns generator object
Not exactly what we want. But it sort of makes sense since the first yield returns a yield. So we process each yield as a generator object? Lets see:
var u = upper(['aaa','bbb','ccc']);
console.log(u.next().value.next().value.next().value); // works
console.log(u.next().value.next().value.next().value); // doesn't work
OK. Let's figure out why the second call doesn't work.
The upper function:
function *upper (items) {
yield* items.mapGen(/*...*/);
}
yields the return value of mapGen(). For now, let's ignore what mapGen does and just think about what yield actually means.
So the first time we call .next() the function is paused here:
function *upper (items) {
yield* items.mapGen(/*...*/); // <----- yields value and paused
}
which is the first console.log(). The second time we call .next() the function call continue at the line after the yield:
function *upper (items) {
yield* items.mapGen(/*...*/);
// <----- function call resumes here
}
which returns (not yield since there's no yield keyword on that line) nothing (undefined).
This is why the second console.log() fails: the *upper() function has run out of objects to yield. Indeed, it only ever yields once so it has only one object to yield - it is a generator that generates only one value.
OK. So we can do it like this:
var u = upper(['aaa','bbb','ccc']);
var uu = u.next().value; // the only value that upper will ever return
console.log(uu.next().value.next().value); // works
console.log(uu.next().value.next().value); // works
console.log(uu.next().value.next().value); // works
Yay! But, if this is the case, how can the innermost yield in the callback work?
Well, if you think carefully you'll realize that the innermost yield in the callback also behaves like the yield in *upper() - it will only ever return one value. But we never use it more than once. That's because the second time we call uu.next() we're not returning the same callback but another callback which in turn will also ever return only one value.
So it works. Or it can be made to work. But it's kind of stupid.
Conclusion:
After all this, the key point to realize about why yield doesn't work the way we expected is that yield pauses code execution and resumes execution on the next line. If there are no more yields then the generator terminates (is .done).
Second point to realize is that callbacks and all those Array methods (.map, .forEach etc.) aren't magical. They're just javascript functions. As such it's a bit of a mistake to think of them as control structures like for or while.
Epilogue
There is a way to make mapGen work cleanly:
function upper (items) {
return items.mapGen(function (item) {
try {
return item.toUpperCase()
} catch (e) {
return 'null'
}
})
}
var u = upper(['aaa','bbb','ccc']);
console.log(u.next().value);
console.log(u.next().value);
console.log(u.next().value);
But you'll notice that in this case we return form the callback (not yield) and we also return form upper. So this case devolves back into a yield inside a for loop which isn't what we're discussing.
You can use another method by "co - npm": co.wrap(fn*)
function doSomething(){
return new promise()
}
var fn = co.wrap(function* (arr) {
var data = yield arr.map((val) => {
return doSomething();
});
return data;
});
fn(arr).then(function (val) {
consloe.log(val)
});