Skip to content

Instantly share code, notes, and snippets.

@dfkaye
Last active September 25, 2021 00:42
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dfkaye/d0e1ee0448854b778cdcd089c479da70 to your computer and use it in GitHub Desktop.
Save dfkaye/d0e1ee0448854b778cdcd089c479da70 to your computer and use it in GitHub Desktop.
Notes from Bodil Stokke's "The Miracle of Generators" presentation from the Front Conference Zurich

Notes from Bodil Stokke's "The Miracle of Generators"

Presented at Front Conference Zurich 31 August - 1 September 2017

Video at https://vimeo.com/232221648

Our little data

var ponies = [
  "AppleJack",
  "Fluttershy",
  "Pinkie Pie",
  "Rainbow Dash",
  "Rarity",
  "Twilight Sparkle"
];

Iterators

With ES6 (aka ES2015), objects have methods .entries(), .keys(), .values(), which return iterators.

var i = ponies.values();

Iterators can be traversed manually using the .next() method.

console.log(i.next());
// { value: "AppleJack", done: false }

console.log(i.next());
// { value: "Fluttershy", done: false }

console.log(i.next());
// { value: "Pinkie Pie", done: false }

console.log(i.next());
// { value: "Rainbow Dash", done: false }

console.log(i.next());
// { value: "Rarity", done: false }

console.log(i.next());
// { value: "Twilight Sparkle", done: false }

// When the iteration is exhausted, .next() returns { done: true }

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

You can use the for ... of construct to iterate all values

for (let i of ponies) { // or `ponies.values()`
  console.log(i);
}
/*
  AppleJack
  Fluttershy
  Pinkie Pie
  Rainbow Dash
  Rarity
  Twilight Sparkle
 */

Side note

Using .entries() produces an array at each step, containing the index and the value.

for (let i of ponies.entries()) {
  console.log(i);
}

/*
  [ 0, "AppleJack" ]
  [ 1, "Fluttershy" ]
  [ 2, "Pinkie Pie" ]
  [ 3, "Rainbow Dash" ]
  [ 4, "Rarity" ]
  [ 5, "Twilight Sparkle" ]
 */

Using Symbol.iterator calls on objects is the "official" way to create an iterator.

var i = ponies[Symbol.iterator]();

for (let i of ponies) { // or `ponies.values()`
  console.log(i);
}
/*
  AppleJack
  Fluttershy
  Pinkie Pie
  Rainbow Dash
  Rarity
  Twilight Sparkle
 */

Custom Iterators

var ponies = {
  // The iterator key is the arrow function that, when called, returns the it object with its own next() method.
  [Symbol.iterator]: () => {
    let c = 0;

    let it = {
      next: () => {
        // a little state machine...
        c += 1;

        switch (c) {
          // case 0: return { value: true };
          case 1: return { value: "AppleJack", done: false };
          case 2: return { value: "Fluttershy", done: false };
          default: return { done: true };
        }
      }
    };

    return it;
  }
};

var i = ponies[Symbol.iterator]();

for (let i of ponies) {
  console.log(i);
}
/*
  AppleJack
  Fluttershy
 */

Streams

Define an infinite stream of numbers...

var infinity = {
  [Symbol.iterator]: () => {
    let c = 0;

    let it = {
      next: () => { 
        return { value: c++, done: false }
      }
    }

    return it;
  }
};

Try it manually...

var i = infinity[Symbol.iterator]();

console.log(i.next());
// { value: 0, done: false }

console.log(i.next());
// { value: 1, done: false }

console.log(i.next());
// { value: 2, done: false }

console.log(i.next());
// { value: 3, done: false }

console.log(i.next());
// { value: 4, done: false }

WHAT ABOUT...?

Do not try this at home, thank you.

for (let i of infinity) {
  console.log(i);
}

Lazy evaluation

We can iterate over the stream with another iterator that "takes" the first X values

var take = function(num, iterable) {
  let o = {
    [Symbol.iterator]: () => {
      let c = 0;
      let it = iterable[Symbol.iterator]()

      return {
        next: () => {
          return c++ < num ? it.next() : { done: true };
        }
      };
    }
  };

  return o;
};

for (let i of take(5, infinity)) {
  console.log(i);
}
/*
  0
  1
  2
  3
  4
 */

Function application with map

var map = function(fn, iterable) {
  let o = {
    [Symbol.iterator]: () => {
      let it = iterable[Symbol.iterator]()

      return {
        next: () => {
          let next = it.next();
          return next.done ? next : { done: false, value: fn(next.value) };
        }
      };
    }
  };

  return o;
};

var fn = (i) => { return i + 5 };
var it = take(5, infinity);
for (let i of map(fn, it)) {
  console.log(i);
}
/*
  5
  6
  7
  8
  9
 */

Composable!

for (let i of take(5, map(i => i + 5, infinity))) {
  console.info(i);
}

/*
  5
  6
  7
  8
  9
 */

However...

This way of writing iterators is clunky, full of boilerplate.

This is where generators come in

A generator is a special function with an asterisk, that generates an iterator - instead of using [Symbol.iterator] notation.

var infinity = function* () {
    let c = 0;
    while (true) {
        yield c++;
    }
}

Try it manually...

No [Symbol.iterator] call required - but we have to call infinity() instead.

var i = infinity();

console.log(i.next());
// { value: 0, done: false }

console.log(i.next());
// { value: 1, done: false }

console.log(i.next());
// { value: 2, done: false }

console.log(i.next());
// { value: 3, done: false }

console.log(i.next());
// { value: 4, done: false }

take() as a generator

var take = function* (num, iter) {
  let c = 0;
  let next;

  while(c++ < num && !(next = iter.next()).done) {
    yield next.value;
  }
};

Note that we have to call infinity(), not just pass it in...

for (let i of take(5, infinity())) {
  console.log(i);
}
/*
  0
  1
  2
  3
  4
 */

map() as a generator

var map = function* (fn, iter) {
  let next;

  while(!(next = iter.next()).done) {
    yield fn(next.value);
  }
};

for (let i of map(i => i + 'lol', take(5, infinity()))) {
  console.log(i);
}
/*
  0lol
  1lol
  2lol
  3lol
  4lol
 */

Generators can accept values, too

var fiveup = function* () {
    let c = 0;
    while (true) {
        // lefthand c maps to a parameter passed in by `next()`
        c = yield c;
        c = c + 5;
    }
}

var i = fiveup();

NOTE: The first next() cal primes the iterator (so that c is 0, and the generator awaits the next iteration).

i.next();

Now pass in some arguments

console.log(i.next(4));
// { value: 9, done: false }

console.log(i.next(100));
// { value: 105, done: false }

Getting useful with Promises (oh no...)

var unit = v => new Promise(res => res(v));

console.log(unit(5));
// Promise { <state>: "fulfilled", <value>: 5 }

console.log(Promise.resolve(5));
// Promise { <state>: "fulfilled", <value>: 5 }

Promise generator...

var promises = function* () {
    console.log( yield unit("omg") )
    console.log( yield unit("wtf") )
    console.log( yield unit("bbq") )
}

var i = promises();

// prime it first...
console.log(i.next());

console.warn(i.next('omg'));
// omg
// { value: Promise { "fulfilled" }, done: false }

console.log(i.next('wtf'));
// wtf
// { value: Promise { "fulfilled" }, done: false }

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

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

Resolve promises...

var run = (iter, val = null) => {
    let next = iter.next(val);
    
    if (!next.done) {
        // Note the recursive call on run()
        next.value.then(result => run(iter, result))
    }
}

run(promises());
/*
  omg
  wtf
  bbq
 */

fetch()

var fallout = function*(url) {
    let response = yield fetch(url);
    let text = yield response.text();
    console.log( text );
}

run(fallout('/generators/data/fallout.txt'));

No more then boilerplate.

Maybe, Prop...

This section required some revision, especially the promise resolver and prop value check.

// DOES NOT WORK IN BROWSERS 4 SEPT 2019
// Promise.defer HAS BEEN DEPRECATED.
var run = (iter, val = null, done = Promise.defer()) => {
  const next = iter.next(val);
  if (!next.done) {
    next.value.then(result => run(iter, result, done));
  } else {
    done.resolve(next.value);
  }
  return done.promise;
};

// SIMPLIFY THE DONE WITH THIS INSTEAD
var run = function(iter, val) {
    let next = iter.next(val);

    if (next.done) {
      return Promise.resolve(val)
    }

    // Note the recursive call on run()
    next.value.then(result => run(iter, result));
};



var ponies = {
    pie: "Pony Pie",
    dash: "Rainbow Dash"
};


run(function* () {
    let dash = ponies.dash;
    let pie = ponies.pie;
    console.log(`${ dash } is friends with ${ pie }`)
}());
// Rainbow Dash is friends with Pony Pie

maybe()

var maybe = val => {
    return {
        then: (fn) => {
            return val != null ? fn(val) : null;
        }
    }
};

console.warn(maybe(ponies.pie).then(value => value));
// Pony Pie

prop()

// ReferenceError warning if key not in obj
var prop = (key, obj) => maybe(obj[key]);

// Fix that with... 
var prop = (key, obj) => {
    return key in obj ? maybe(obj[key]) : maybe(null);
}

console.log(prop("dash", ponies).then(value => value));
// Rainbow Dash

console.log(prop("twi", ponies).then(value => value));
// null

run(function* () {
    let dash = yield prop("dash", ponies);
    let pie = yield prop("pie", ponies);
    console.log(`${ dash } is friends with ${ pie }`)
}());

// Rainbow Dash is friends with Pony Pie

run(function* () {
    let dash = yield prop("dash", ponies);
    let twi = yield prop("twi", ponies);

    // SHOULD NOT PRINT BECAUSE maybe().then() does not call fn() on null!
    console.log(`${ dash } is friends with ${ twi }`)
}());

// SHOULD *NOT* SEE THIS from the prop('twi', ponies) call:
// ReferenceError: reference to undefined property "twi"

Which are really...

Monads.

{
    unit: the unit function ??? - var unit = (v) => new Promise(res => res(v));

    bind: the `run` function ??? - I DON'T KNOW
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment