Coding recipe: extracting loop functionality (via callbacks and generators)

May 01, 2018 0 Comments

Coding recipe: extracting loop functionality (via callbacks and generators)

 

 

In this blog post, we look at two ways of extracting the functionality of a loop: internal iteration and external iteration.

The loop  #

As an example, take the following function logFiles():

const fs = require('fs');
const path = require('path'); function logFiles(dir) { for (const fileName of fs.readdirSync(dir)) { const filePath = path.resolve(dir, fileName); console.log(filePath); const stats = fs.statSync(filePath); if (stats.isDirectory()) { logFiles(filePath); } }
}
logFiles(process.argv[2]);

The loop starting in line A logs file paths. It is a combination of a for-of loop and recursion (the recursive call is in line B).

What if you find the functionality of the loop (iterating over files) useful, but don’t want to log?

Internal iteration  #

The first option for extracting the loop functionality is internal iteration:

const fs = require('fs');
const path = require('path'); function logFiles(dir, callback) { for (const fileName of fs.readdirSync(dir)) { const filePath = path.resolve(dir, fileName); callback(filePath); const stats = fs.statSync(filePath); if (stats.isDirectory()) { logFiles(filePath, callback); } }
}
logFiles(process.argv[2], p => console.log(p));

This way of iterating is similar to the Array method .forEach(): logFiles() implements the loop and invokes its callback for every iteration value (line A).

External iteration  #

An alternative to internal iteration is external iteration: We implement an iterable and a generator helps us with it:

const fs = require('fs');
const path = require('path'); function* logFiles(dir) { for (const fileName of fs.readdirSync(dir)) { const filePath = path.resolve(dir, fileName); yield filePath; const stats = fs.statSync(filePath); if (stats.isDirectory()) { yield* logFiles(filePath); } }
}
for (const p of logFiles(process.argv[2])) { console.log(p);
}

With internal iteration, logFiles() called us (“push”). This time, we call it (“pull”).

Note that in generators, you must make recursive calls via yield* (line A): If you just call logFiles() then it returns an iterable. But what we want is to yield every item in that iterable. That’s what yield* does.

One nice trait of generators is that processing is just as interlocked as with internal iteration: Whenever logFiles() has created another filePath, we get to look at it immediately, then logFiles() continues. It’s a form of simple cooperative multitasking, with yield pausing the current task and switching to a different one.

Further reading  #


Tag cloud