Iterable and Iterators
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:
- zero arg function that returns an object with at least the following two
properties:
- done (boolean) — true if iterator past the end of the iterated sequence, false if the iterator was able to produce the next value in the sequence.
- value - any value JS value (can be omitted when done is true);
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 😁.
// 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.