NAZAR ILKIV

Feb 2, 2021

Javascript Iterators & Generators

Iterators

In Javascript an iteratorΒ is an object which defines a sequence and potentially a return value upon its termination. Specifically, an iterator is any object which implements the Iterator protocol

MDN

Iterator Protocol

Convention !!!

type IteratorResult = {
  value: any,
  done: boolean,
};

interface Iterator {
    next(): IteratorResult;
}

Iterator Example

function createIterator() {
  let index = 0;

  return {
    next() {
      return {
        value: index++,
        done: false,
      }
    }
  }
}

const iterator = createIterator();

console.log(iterator); // => {next: Ζ’ next()}
console.log(iterator.next()); // => {value: 0, done: false}
console.log(iterator.next()); // => {value: 1, done: false}
console.log(iterator.next()); // => {value: 2, done: false}
console.log(iterator.next()); // => {value: 3, done: false}

Iterator Features

Stateful

Lazy

Ok, what's next ?!

Almost nothing!!! πŸŽ‰πŸ‘πŸ‘

Iterables

An object is iterableΒ if it defines its iteration behavior

Iterable Protocol

Convention !!!

interface Iterable {
    [Symbol.iterator](): Iterator;
}

Built-in iterables

String

Array

Map

Set

Object ???

Consuming Iterables

Spread

for ... of

Array.from()

Map() constructor

const str = 'Hello';
const arr = [1, 2, 3];

console.log([...str]) // => ['H', 'e', 'l', 'l', 'o']
console.log([...arr]) // => [1, 2, 3]; arr !== result

for (const it of str) {
  console.log(it); // prints 'H', 'e', 'l', 'l', 'o',
}

for (const it of arr) {
  console.log(it); // prints 1, 2, 3
}

Example

const iterator = [1,2,3][Symbol.iterator]();

console.log(iterator.next); // => Ζ’ next() {}
console.log(iterator.next()); // => {value: 1, done: false}
console.log(iterator.next()); // => {value: 2, done: false}
console.log(iterator.next()); // => {value: 3, done: false}
console.log(iterator.next()); // => {value: undefined, done: true}

Example

Slides are boring...
Let's code!!!
πŸ‘¨β€πŸ’»

Generators

Special syntax for creating iterators

function* customGenerator() {
  yield 1;
  yield 2;
  yield 3;
}

console.log(typeof customGenerator); // => 'function'

How it works?

function* customGenerator() {
  yield 'Hello';
  yield Math.random();
  
  return [1,2,3];
}

const result = customGenerator();
console.log(result); // => GeneratorFunctionPrototype
console.log(result[Symbol.iterator]);
// => Ζ’ [Symbol.iterator]() { [native code] }
console.log(result.next); // => Ζ’ next() { [native code] }

console.log(result.next()) // => {value: "Hello", done: false}
console.log(result.next()) // => {value: 0.35061525498851975, done: false}
console.log(result.next()) // => {value: Array(3), done: true}
console.log(result.next()) // => {value: undefined, done: true}

Features

Iterable

Lazy

Composable

Can consume values

Iterate

function* myGenerator() {
    yield 1
    yield 2
    yield 3
    return 4
}

const gen = myGenerator()

for (let val of gen) {
    console.log(val) // prints 1, 2, 3
}

// if iterable let's try to spread it
const gen2 = myGenerator()
console.log([...gen2]) // prints [1, 2, 3]

Lazy Evaluation

function* random() {
    while(true) yield Math.random()
}
function* filter(items, predicate) {
    for(let item of items) {
        if(predicate(item)) yield item
    }
}
function* take(items, number) {
    let count = 0

    for(let item of items) {
        yield item
        count++
        if(count >= number) return
    }
}
const items = [...take(filter(random(), n => n < 0.5), 2)]
// [0.04971046381666788, 0.3152250891138142]

Composition

function* oneTwo() {
    yield 1;
    yield 2;
}

function* fourFive() {
    yield 4;
    yield 5;
}

function* sequenceToFive() {
    yield 0;
    yield* oneTwo();
    yield 3;
    yield* fourFive();
}

const sequence = sequenceToFive();
console.log([...sequence]); // prints [0, 1, 2, 3, 4, 5]

Data Consuming

function* firstLastNameComposer() {
    const firstName = yield 'What is your first name?';
    const lastName = yield 'What is your last name?';

    return `${firstName} ${lastName}`;
}

const gen = firstLastNameComposer()

console.log(gen.next())
// { value: 'What is your first name?', done: false }
console.log(gen.next('Vasya'))
// { value: 'What is your last name?', done: false }
console.log(gen.next('Pupkin'))
// { value: 'Vasya Pupkin', done: true }

Handling Rejections

function* generator() {
  try {
    const obj = {};

    obj.foo.bar;
    
    yield 1;
  } catch (e) {
    console.log("generator error", e);
  }
}

const gen = generator();
// generator error 
// TypeError: Cannot read property 'bar' of undefined

console.log(gen.next());
// {value: undefined, done: true}

Handling Rejections

function* generator() {
  try {
    yield 1;
    
    console.log('Hi from generator!'); // never invoked
  } catch (e) {
    console.log("generator error", e);
  }
}

const gen = generator();
gen.next();

console.log(gen.throw(new Error('This is an error')));

Real World Examples

🌍

Plain Async Code

function* getUserGHRepoCommits(username) {
    const API_URL = 'https://api.github.com'

    const reposFetch = yield fetch(`${API_URL}/users/${username}/repos`)
    const repos = yield reposFetch.json()
    const repoCommitsFetch =
        yield fetch(`${API_URL}/repos/${username}/${repos[0].name}/commits`)
    
    return repoCommitsFetch.json()
}

const gen = getUserGHRepoCommits('ekscentrysytet')

gen.next().value
  .then(reposFetch => gen.next(reposFetch).value)
  .then(repos => gen.next(repos).value)
  .then(commitsFetch => gen.next(commitsFetch).value)
  .then(commits => console.log(commits)) // prints [Object, Object, Object, Object]

CO

const co = require('co')
const express = require('express')
const router = express.Router()
const Post = require('../models/Post');
/* ... */
router.put('/:postId', auth, (req, res, next) => {
    co(function* () {
      const post = yield Post.findOne({id: req.params.postId});
    
      if (!post) throw new Error('postId');
    
      if (req.user.id !== post.author.toString()) {
        throw new Error('noRights');
      }
    
      const updatedPost = yield post.update(req.body);
      return updatedPost;
    })
      .then(result => res.json(result))
      .catch(err => {    
        res.status(errCode).json(err);
      })
})

Async Iterators

interface IteratorResult {
    value: any;
    done: boolean;
}

interface AsyncIterator {
    next() : Promise<IteratorResult>;
}

interface AsyncIterable {
    [Symbol.asyncIterator]() : AsyncIterator;
}
for await (const item of asyncIterable) {
  // ...
}

Consuming streams

async function streamingRead() {
  const response = await fetch('https://html.spec.whatwg.org');

  for await (const chunk of streamAsyncIterator(response.body)) {
    console.log(`Read ${chunk.length} bytes`);
  }
}

async function* streamAsyncIterator(stream) {
  const reader = stream.getReader();

  try {
    while (true) {
      const {done, value} = await reader.read();
      // Exit if we're done
      if (done) return;
      // Else yield the chunk
      yield value;
    }
  }
  finally {
    reader.releaseLock();
  }
}

streamingRead();

Consuming paginated API's

async function* fetchCommits(repo) {
  let url = `https://api.github.com/repos/${repo}/commits`;

  while (url) {
    const response = await fetch(url, {
      headers: {'User-Agent': 'Our script'},
    });

    const body = await response.json();

    let nextPage = response.headers.get('Link').match(/<(.*?)>; rel="next"/);
    nextPage = nextPage && nextPage[1];

    url = nextPage;

    for (let commit of body) {
      yield commit;
    }
  }
}

// then
for await (const commit of fetchCommits('javascript-tutorial/en.javascript.info')) {
  console.log(commit);
}

Q&A Time

Javascript Iterators & Generators

By Nazar Ilkiv

Javascript Iterators & Generators

  • 336