JavaScript Generators

JavaScript Generators

1. function* agenda() {
2. yield "obligatory introduction";
3. yield "new syntax";
4. yield "basics";
5. yield "asynchronous behaviour";
6. yield "flow control from the caller side";
7. yield "generator use-case: early implementation of async/await";
8. yield "generator use-case: sagas";
9. }
10.

About me

  • Lenz Weber (@phry on Twitter, phryneas everywhere else)
  • Developer at
  • Working in professional web development since 15 years
  • In love with the Web, Open Source, Linux and Security
  • Just getting used to conference speaking, so bear with me

Let's get to know Generators

Syntax Basics

1. function* myFirstGenerator() {
2. yield "some-return-value";
3. yield "another-return-value";
4. }
5.
6. const myGeneratorInstance = myFirstGenerator();

 

Usage Basics: the for-of-loop

1. function* myFirstGenerator() {
2. yield "some-return-value";
3. yield "another-return-value";
4. return "then-we-really-return";
5. }
6.
7. for (const value of myFirstGenerator()) {
8. console.log(value);
9. }
10.

Usage Basics: using the Iterable protocol

1. function* myFirstGenerator() {
2. yield "some-return-value";
3. yield "another-return-value";
4. return "then-we-really-return";
5. }
6.
7. const iterableObject = myFirstGenerator();
8.
9. let nextValue = iterableObject.next();
10. while (!nextValue.done) {
11. console.log(nextValue.value);
12.
13. nextValue = iterableObject.next();
14. }
15.

Asynchronous Behaviour: yield steps out of the Generator

1. function* myGenerator() {
2. console.log("generator started");
3. yield "some-yielded-value";
4.
5. console.log("continuing");
6. yield "another-yielded-value";
7.
8. console.log("continuing");
9. return "then-we-really-return";
10. }
11.
12. let nextValue;
13. const iterableObject = myGenerator();
14. console.log("generator created");
15. nextValue = iterableObject.next();
16. console.log("got first yielded value", nextValue);
17. nextValue = iterableObject.next();
18. console.log("got second yielded value", nextValue);
19. nextValue = iterableObject.next();
20. console.log("got third value", nextValue);

 

Flow Control from the Caller

Flow Control from the Caller: passing values into a running Generator Function

1. function* sumGenerator() {
2. let sum = 0;
3. while (true) {
4. const value = yield sum;
5. console.log("got passed", value);
6. sum += value;
7. }
8. }
9.
10. const iterableObject = sumGenerator();
11. for (const add of [1, 2, 3]) {
12. const returnedValue = iterableObject.next(add).value;
13. console.log("returned value is", returnedValue);
14. }
15.

The chicken-egg-problem of passing values in both directions

If we pass in a value to be used before the first yield and at the same time use the yield keyword to access values passed in, we can never access the first value that is passed in.

⇒ The first time you call Iterator.prototype.next(value)
value goes straight to /dev/null!

Possile solutions:

  • invert the problem: call next once just to get the generator started, without using the return value.
  • don't pass the first value to next, but as an argument to the Generator Function call itself.

Both solutions require the user of your generator to use it accordingly, so write some documentation.

Example for the "inverted" solution

1. function* sumGenerator() {
2. let sum = 0;
3. while (true) {
4. const value = yield sum;
5. console.log("got passed", value);
6. sum += value;
7. }
8. }
9.
10. const iterableObject = sumGenerator();
11. iterableObject.next();
12. for (const add of [1, 2, 3]) {
13. const returnedValue = iterableObject.next(add).value;
14. console.log("returned value is", returnedValue);
15. }

 

Example for the "function argument" solution

1. function* sumGenerator(firstPassedValue) {
2. let sum = firstPassedValue;
3. while (true) {
4. const value = yield sum;
5. console.log("got passed", value);
6. sum += value;
7. }
8. }
9.
10. const iterableObject = sumGenerator(1);
11. for (const add of [undefined, 2, 3]) {
12. const returnedValue = iterableObject.next(add).value;
13. console.log("returned value is", returnedValue);
14. }

 

Flow Control from the Caller: injecting an Error

1. function* generateFib() {
2. let a = 0,
3. b = 1;
4. try {
5. while (true) {
6. yield a;
7. const _ = a + b;
8. a = b;
9. b = _;
10. }
11. } catch (e) {
12. console.log("guess I went too far.");
13. }
14. }
15.
16. const iterableFib = generateFib();
17. for (const fib of iterableFib) {
18. console.log(fib);
19. if (fib > 100) {
20. iterableFib.throw("stop this nonsense!");
21. }
22. }
23.

Flow Control from the Caller: forcing to return

1. function* generateFib() {
2. let a = 0,
3. b = 1;
4. try {
5. while (true) {
6. try {
7. yield a;
8. } catch (e) {
9. console.log("you will never stop me with", e);
10. }
11. const _ = a + b;
12. a = b;
13. b = _;
14. }
15. } finally {
16. console.log("oh, you got me nonetheless.");
17. }
18. }
19.
20. const iterableFib = generateFib();
21. for (const fib of iterableFib) {
22. console.log(fib);
23. iterableFib.throw("stop.");
24. if (fib > 100) {
25. iterableFib.return("stop this nonsense!");
26. // returns { done: true, value: "stop this nonsense!" }
27. }
28. }
29.

As a sidenote

  • you can override the return value that is forced by myIterator.return("forced-return-value")
    by returning something different in your finally block
  • with yield*, you can delegate to another generator (stuff like "throw" will control the inner generator until it is done!)

Let's look at real-world use cases

Use Case: simulating async-await-like behaviour

what is async-await?

1. // before async/await
2. function fetchMovieTitles() {
3. return fetch("http://example.com/movies.json")
4. .then(function(response) {
5. return response.json();
6. })
7. .then(function(json) {
8. return json.map(movie => movie.title);
9. });
10. }
11. const movieTitlePromise = fetchMovieTitles();
12.
13. // with async await
14. async function fetchMovieTitles() {
15. const response = await fetch("http://example.com/movies.json");
16. const json = await response.json();
17. return json.map(movie => movie.title);
18. }
19. const movieTitlePromise = fetchMovieTitles();
20.
  • async-await was part of ES2017
    landed in Chrome 55, Firefox 52, Edge 15 (2016/2017)
  • generators were already part of ES2015
    available since Chrome 39, Firefox 26, Edge 13 (2013-2015)
  • in the meantime, people got creative
  • so what did that look like?

How we want to use it

1. // with async await
2. async function fetchMovieTitles() {
3. const response = await fetch("https://example.com/movies.json");
4. const json = await response.json();
5. return json.map(movie => movie.title);
6. }
7. const movieTitlePromise = fetchMovieTitles();
8.
9. // with our solution
10. function* fetchMovieTitles() {
11. const response = yield fetch("https://example.com/movies.json");
12. const json = yield response.json();
13. return json.map(movie => movie.title);
14. }
15.
16. const movieTitlePromise = runAsync(fetchMovieTitles());
17.
18. function runAsync(generator) {
19. function run(arg) {
20. const nextValue = generator.next(arg);
21.
22. if (nextValue.done) {
23. return nextValue.value;
24. }
25. return Promise.resolve(nextValue.value).then(run);
26. }
27.
28. return run();
29. }

Let's look at real-world use cases

Use Case: Sagas

Sagas

  • are triggered from asynchronous actions
  • call "Effects" in sequence
  • an Effect is the description of how a synchronous or asynchronous action should be executed
    (other sagas can be called using Effects, too!)
  • the real execution of Effects is done by the runtime
  • can be cancelled from the outside
    (for example if we want a saga to run only once, but a newer asynchronous actions re-starts the saga)
  • can "clean up" after themselves if they are cancelled

Note: Sagas are available for many different platforms, we will look at redux-saga here

Saga example: AutoComplete in redux-saga

1. import { takeLatest, put, call, race, cancelled,delay } from "redux-saga/effects";
2.
3. const api = "//api.example.com/";
4.
5. export default function* autoCompleteSaga() {
6. yield takeLatest("autoComplete/keyDown", updateAutoComplete);
7. }
8.
9. function* updateAutoComplete(action) {
10. try {
11. const { results, timeout } = yield race({
12. results: call(getData, `${api}/cityNames/${action.payload.inputValue}`),
13. timeout: call(delay, 10000)
14. });
15.
16. if (timeout) {
17. throw new Error("timed out after 10 seconds");
18. }
19.
20. yield put({ type: "autoComplete/setNewValues", payload: results });
21. } catch (e) {
22. yield put({ type: "autoComplete/fetchFailed", e });
23. }
24. }
25.
26. function* getData(url) {
27. const response = yield call(fetchData, url);
28. if (response.status === 200) {
29. const json = yield call([response, response.json]);
30. return json;
31. } else {
32. throw new Error("bad return code");
33. }
34. }
35.
36. function* fetchData(url) {
37. const abortController = new AbortController();
38. try {
39. const result = yield call(fetch, url, { signal: abortController.signal });
40. return result;
41. } finally {
42. if (yield cancelled()) {
43. abortController.abort();
44. }
45. }
46. }

Time for Questions & Discussion

By the way...

We are hiring!

(talk to me)