Wednesday, June 8, 2016

Refactoring and Iterating on Generators

In this post, I delve into writing the same functionality in a myriad of ways. In the process, I'll evaluate some trade-offs, as well as try out generators of Javascript ES6.

The problem I'm solving is simple: we're building an interaction that plays musical sounds based on the users' actions. The details aren't important here, but I need to generate the frequencies for six octaves of "A" notes. My pair programmer (Aila) and I write simply:

const octaves = [110, 220, 440, 880, 1760, 3520]

Refactor, pass I

This is OK, but not as maintainable as it should be. To change the key of the notes, my future self willg have to change all the numbers. I’d rather express the underlying math if possible.

Turns out that this can be done with an old fashion for loop.

const octaves = buildOctaves(110, 4000) // "110" is base/bass note

function buildOctaves(lo, hi) { 
  
  const o = []
  
  for (let f = lo; f < hi; f *= 2)
    
    o.push(f)
  
  return o

}


That works. It's not super because of all the noise there-- the temporary const and the for-loop hide the * 2 which is the essence of it. Does recursive work better? No, but sometimes you don't know until you try:

function buildOctavesRecursive(lo, hi) {
  return (function next(f) {
    if (f < hi) {
      const n = next(f*2)
      return n ? [f].concat(n) : [f]
    }
  })(lo)
}

ES6

I’ve been using RxJS lately, and thought about using it. It's great at generating sequences and would handle this fine. But RxJS's sweet spot is asynchronous event streams, and this is much simpler. In fact, it seems like a great fit for the new iterators and generators functionality in ES6.

I liked the idea of using the spread (...) operator to execute the loop, to get rid of the while construct. I find some examples of building generators online, and after realizing I don't need to use [Symbol.iterator], I get:

function buildOctaves(lo, hi) { // from Generator, take II
  const iterator = function* () {
    let a = lo
    while (a < hi) {
      yield a
      a *= 2
    }
  }
  return [...iterator()]
}

I still have a while loop, but this is readable and transparent about the intent of the code. I even like the inlined version:

function buildOctavesGenerator(lo, hi) {
  return [...(function* () {
    let a = lo
    while (a < hi) {
      yield a
      a *= 2
    }
  })()]
}

It's nice that the different aspects of the logic are each on their own expressive line:

  • return [...function* () () { -- return an array generated by...
  • while (a < hi) -- while our accumulator is below the limit
  • a *= 2 -- multiply by two

A note about using new features: Admittedly if the new features of Javascript are new to you, this is the least readable. But we need to be careful here. We need to ask, is it hard to read because of unfamiliar language features, or how the logic is spelled out? With a few exceptions, we need to assume that the reader understands the features of the language. Otherwise, we go down the path of writing code for the least common denominator: we limit ourselves to a small, arbitrary subset of any programming language. (The exception of this is complex languages like C++, where a style guide is needed to highlight which features of the language that one particular project will use.)

But I want to try one more pass, and see if the logic can be made clearer. The while and yield are a distraction from the simple math here.

Top down

To get at the most readable code, often you need to start with the final desired ideal code. So I sketch out what this might look like:

function buildOctaves(lo, hi) {
  const octaveIter = doubleIter(lo)
  const notes = whileLessThan(octaveIter, hi)
  
  return [...notes]
}


The whole octave math is based on doubling the lower octave, so it makes sense as a basic generator to build upon. That needs to be coupled with something to halt the iteration.

The doubling iterator is obvious:

const doubleIter = function* (x) {
  while (true) {
    yield x
    x *= 2
  }
}

Nice! And the whileLessThan can be:

const whileLessThan = function*(it, max) {
  for (x of it) {
    if (x < max)
      yield x;
    else
      return
  }
}

This is okay, but I realized that this function is doing two different things, and parameterizing the loop with a function allows us to do the same thing with functional composition:

const takeWhile = function* (it, fn) {
  for (x of it) {
    if (fn(x))
      yield x;
    else
      return
  }
}

const whileLessThan = function(it, max) {
  return takeWhile(it, (x) => x < max)
}

Conclusion

For me, since the last few functions I extracted are semantically clear and can be moved into a library, the decomposed solution using the latest Javascript generators really is the best one. It provides the clearest abstraction of logic as well as the easiest refactor paths.

What do you think?

Post a Comment