Taran_logo_black
TILBLOGABOUTUSES

Iterable and Iterators

Taran "tearing it up" Bains • November 11, 2019 6 min read

Today I’d like to discucss iterators and iterables! You know that they are different things but if you’re like me, you can never quite remember which is which. First and foremost, they aren’t unrelated and secondly they do have one similiarity… they are both protocols (a set of rules) that can be applied to objects in JavaScript. I’ll also delve a little bit into Generators and how they relate to the two aformentioned topics.

Iterator Protocol

The iterator protocol defines a standard way to produce a sequence of values (finitie or infinite), and maybe return a value when all values have been generated. They allow us to access one item from a collection of items, whilst keeping track of the current position.

An object is an iterator when it implements a next() method with the following semantics:

Next:

next will throw a TypeError if a non-object value is returned.

Iterable Protocol

In JavaScript, the iterable protcol defines custom iteration behavior. When we’re looping over an array with a for of, what’s going on under the hood is that we’re actually using the built in iterator method that ships with Arrays. Whenever an iterable is iterated over, its iteartor method is called with no arguments, and its returned iterator is used to obtain the values to be iterated.

In order for something to be an iterable in JavaScript, the object must have a property with a @@itearator key (this can be done via Symbol.iterator).

var taran = {
  [Symbol.iterator]() {
    let i = 0
    return {
      next() {
        while (i < 5) {
          ++i
          return {
            value: i,
            done: false,
          }
        }

        return {
          done: true,
        }
      },
    }
  },
}

var it = taran[Symbol.iterator]()
console.log(it.next().value) // 1
console.log(it.next().value) //2

Generators

Generators, if you don’t know about them, are an amazing addition to the JavaScript spec! Generators bundled with Promises are the building blocks for async await; you know, that awesome feature that allows us to write asynchronous code in a synchronous manner.

Generators can be thought of as pausable functions that yield control (or the thread of execution) to some other function. They define their own custom iterative algorithm yielding each item in their sequence. They return an iterator when called. The beauty of this feature is that they can decrease the lines of code written for our iterators!

var taran = {
  *[Symbol.iterator]() {
    let i = 0
    while (i < 5) {
      i++
      yield i
    }
  },
}

var it = taran[Symbol.iterator]()
console.log(it.next().value) // 1
console.log(it.next().value) // 2

Generators let us pause exection and do assignments at a later time in our program

function* someRandomGen() {
  let lastResult = 0
  while (true) {
    // if we pass a value to the iterators next function, we can do an assignment.
    lastResult = yield lastResult + 5
    console.log(lastResult)
  }
}

let it = someRandomGen()
console.log(it.next().value)
console.log(it.next(20).value)
console.log(it.next(100).value)

// 5 -- yielded from the generator
// 20 -- yielded from the generator
// 25 -- from the outer console.log
// 100 -- yielded from the generator
// 105 -- from the outer console.log

Generators also allow us to delegate to other generators/iterables

function* func1() {
  yield 42;
}

function* func2() {
  yield* func1();
}

const iterator = func2();

console.log(iterator.next().value);
// expected output: 42

Prior to async await, utilities like

Co and

Bluebird   allowed us to write asynchronous code that appeared to be synchronous! In fact, it’s not so hard to create our own generator consumer (like Co) and I did just that, I wrote my own example that you should be able to run in Node 😁.

taran-gen-consumer
// what do we want this thing to look like
//  it takes in a generator function and each time it encounters a yield
// exit execution and resume when it is time.
const https = require("https")

// our generator consumer
function taran(gen, ...rest) {
  const context = this

  return new Promise((resolve, reject) => {
    // we want to get the next iterator after we have fulfilled the promise
    // each gen call will return a new promise
    let genRef = gen
    if (typeof gen === "function") genRef = gen.apply(context, rest)
    if (!genRef || typeof genRef.next !== "function") return resolve(gen)

    onFulfilled()
    function onFulfilled(result) {
      let retVal
      try {
        // pass in the result of the previous value into the next iterator
        retVal = genRef.next(result)
      } catch (error) {
        reject(error)
      }
      next(retVal)
      return null
    }

    function next(ret) {
      // let's assume we passed in a promise
      const { done, value } = ret
      if (done) return resolve(value)
      const promisedValue = toPromise(value)
      if (isPromise(promisedValue)) {
        return ret.value.then(onFulfilled)
      }
    }
  })
}

// simple utility to convert vals to promises
function toPromise(val) {
  if (isPromise(val)) return val
  // hook this back into our runner and then the next will be called on this puppy
  if (isGen(val)) return taran.call(this, obj)
  if (typeof val === "function") return funcToPromise(val)
  return val
}

// convert a thunk to a promise
function funcToPromise(obj) {
  const context = this
  return new Promise((resolve, reject) => {
    obj.call(context, function cb(err, ...rest) {
      if (err) return reject(err)
      return resolve(...rest)
    })
  })
}

function isGen(val) {
  return typeof val.next === "function" && typeof val.throw === "function"
}

function isPromise(val) {
  return typeof val.then === "function"
}

taran(function* gen() {
  try {
    yield new Promise((resolve, reject) => resolve(console.log(1)))
    yield request("https://api.chucknorris.io/jokes/random").then(data => {
      const parsed = JSON.parse(data)
      console.log(parsed.value)
    })
    yield* someGen()
    yield request("https://api.chucknorris.io/jokes/random").then(data => {
      const parsed = JSON.parse(data)
      console.log(parsed.value)
    })
    return 3
  } catch (err) {
    console.log(err)
  }
})

function* someGen() {
  yield new Promise((resolve, reject) => resolve(console.log(2)))
}

const request = url => {
  return new Promise((resolve, reject) => {
    const req = https.get(url, res => {
      if (res.statusCode < 200 || res.statusCode >= 300) {
        return reject(new Error(`Status Code: ${res.statusCode}`))
      }

      const data = []

      res.on("data", chunk => {
        data.push(chunk)
      })

      res.on("end", () => resolve(Buffer.concat(data).toString()))
    })

    req.on("error", reject)

    // IMPORTANT
    req.end()
  })
}

Closing Thoughts

Abstractions are great and they allow us to quickly develop applications/programs but there is a cost associated with using them. Namely, since we’re able to engage with an idea without understanding what’s going on under the hood, when the abstraction falls apart or if we need to go/delve a little bit deeper and build on top of the abstraction, we are unable to do this because we don’t know have any knowledge regarding the internal plumbing.