Generators

Generators are a language feature of Javascript that essentially allows a function to output multiple values, potentially asynchronously.

ixfx includes:

  • count: yields a series of integers counting up (or down) from zero
  • numericRange: yields a series of numbers with a defined interval, start and end. Can reset back to start and loop
  • ping pong: same as numeric range, but it counts back down to start before looping
  • oscillators: ixfx's oscillators are implemented as generators

Uses:

  • interval: an asynchronous generator, interval calls and returns a result at a specified interval.

Importing tips:

// Import as a module, meaning you have to prefix functions with Generators.
import * as Generators from 'ixfx/lib/generators.js';
// Or, import as a module from the web directly
import * as Generators from "https://unpkg.com/ixfx/dist/generators.js"
// Or, import a single function, eg interval
import { interval } from 'ixfx/lib/generators.js';

Background

Generators are a form of iterator, an object that allows you to traverse - that is, to step through - some other data. Objects are iterable if they provide an iterator on request, that is, the give the possibility for stepping through their contents in some manner. The familiar collections - arrays, maps and so on - are all iterables.

for .. of is the usual way of working with an iterator. In this case, we're iterating over an array (which is iterable):

for (const v of someArray) {

}

Iterators allow us to traverse over data in different order, or perhaps returning different views over the data. For example, Object.keys() and Object.values() both return an iterators over whatever object you provide as a parameter. One yields a series of keys, the other values.

const something = {
  colour: `red`,
  size: 10
}

// Iterate keys (ie fields) of an object
for (const key of Object.keys(something)) {
  // `colour`, `size` ...  
}

// Different iterator yields a series of values,
// even though input is the same
for (const value of Object.values(something)) {
  // `red`, 10 ...
}

Iterators can be used in for .. of loops as above, but it's not always the case that you want to access every item at the same time. For example, maybe you want to fetch a new item from an iterable every minute.

In this case, you can work with the iterator manually. Iterators have a next() function which both moves the iterator to the next position, and returns {done, value}, where done is true/false and value is the current value of iterator.

Here we move the iterator and use the value:

const {value, done} = iter.next();
if (done)  { /* handle when iterator is complete? */ }
else {
  // Use value...
}

That kind of code could be called in a timer, fetching and using the value every x seconds, for example. Below gives an example of setting state.value with a new item from the iterator every 60 seconds. When/if the iterator finishes, it stops the interval and sets the value to undefined.

const iterInterval = setInterval(() => {
  // This code runs every 60secs...
  const {value, done} = iter.next();
  if (!done) {
    // Update `state.value` to the next thing from the iterator
    state = { ...state, value }
  } else {
    // Set `state.value` to undefined and stop interval
    state = { ...state, value: undefined }
    clearInterval(iterInterval);
  }
}, 60*1000); // 60 seconds 

Tip: ixfx's interval makes iterating with delay easy.

Iterables can be converted into an array:

const asArray = Array.from(iterable);

// Or alternatively:
const asArray =[...iterable];

What's interesting about iterables is that they aren't an actual collection or set of things, but rather generate values on-demand. This means it's possible to have an iterable that never ends.

Count

count yields a series of integers, counting by one: 0 1 2 3 ...

As the examples show, count can be a useful way of running a chunk of code x number of times. It might be more readable and robust than a typical do/while or for loop because there's only one thing you need to express: the amount of times to loop.

// repl-pad
import { count } from "https://unpkg.com/ixfx/dist/generators.js"

// count(amount:number, offset:number = 0);
// Yields the array: [ 0, 1, 2, 3, 4 ]
const a = [...count(5)];

Or the more common style for using generators is to loop over them:

// repl-pad
import { count } from "https://unpkg.com/ixfx/dist/generators.js"
for (const i of count(5)) {
  // Loop runs five times, with i being 0, 1, 2, 3 and then 4
  console.log(i);
}

A negative amount counts backwards from zero:

import {count} from "https://unpkg.com/ixfx/dist/generators.js"
import {forEach} from "https://unpkg.com/ixfx/dist/flow.js"

// Prints Hi! 0, Hi! -1 ... Hi! -4
[...count(-5)].forEach(i => {
  console.log(`Hi! ${i}`);
});

If an offset is supplied, it is added to the result:

// Yields [ 1, 2, 3, 4, 5 ]
const a = [...count(5,1)];

For more complicated counting, consider numericRange, which allows you to set the counting interval, and whether counting resets.

Numeric range

numericRange yields a series of numbers from start to end, with a specified interval. Unlike count, it can increment by and return fractional values.

import {numericRange} from "https://unpkg.com/ixfx/dist/generators.js"

// numericRange(interval, start, end, repeating)

// Counts from 0-100, by 0.1
for (const v of numericRange(0.1, 0, 100)) { }

// Counts in twos from 0-100, and repeats from 0 again after 100
for (const v of numericRange(2, 0, 100, true)) { 
  // Caution: this generator never ends by itself, so you need
  // a `break` statement somewhere in the for loop
}

// Generators can be used manually as well...
const range = numericRange(1, 0, 100);
range.next().value;

If you just want to simply count from 0 to some number, consider using count instead.

Numbers.linearSpace generates a set number of steps between a start and end number.

// repl-pad
import { linearSpace } from "https://unpkg.com/ixfx/dist/numbers.js"
// linearSpace(start, end, steps);
// Break up 1..5 in six steps
const values = [...linearSpace(1, 5, 6)];
// Yields: [ 1, 1.8, 2.6, 3.4, 4.2, 5 ]

Percentages

To constrain the range to the percentage scale (0-1), use numericPercent:

import {numericPercent} from "https://unpkg.com/ixfx/dist/generators.js"

// numericPercent(interval, repeating, start, end)

// Counts from 0 to 1 by 10%
for (const v of numericPercent(0.1)) { 
  // 0, 0.1, 0.2 ...
}

// Counts from 0 to 1 by 10%, looping from 0
for (const v of numericPercent(0.1, true)) { 
  // 0, 0.1, 0.2 ... 1.0, 0.0, 0.1, 0.2 ...  
  // Warning: infinite generator, make sure you `break` at some point
}

// Constant rotation
const r = numericPercent(0.1); // Setup once
// Per animation loop, calculate new rotation
const angle = Math.PI*2*r.next().value; 

Ping pong

pingPong is like a repeating numericRange but it counts up and back down again when looping, rather than resetting to the start.

import { pingPong } from "https://unpkg.com/ixfx/dist/generators.js"

// pingPong(interval, start, end, offset)

// Counts up and down to 100 in 10s
for (const v of pingPong(10, 0, 100)) {
  // 0, 10, 20 ... 100, 90, 80 ...0, 10, 20 ...
  // Warning: infinite generator, make sure you `break` at some point
}

pingPongPercent is a variation of pingPong, but it locks everything to a scale of 0-1.

import { pingPongPercent } from "https://unpkg.com/ixfx/dist/generators.js"

for (const v of pingPongPercent(0.01)) {
  // Up and down from 0->1 by 1%
  // Warning: infinite generator, make sure you `break` at some point
}

// Loops between 20-80% by 10%
const pp = pingPongPercent(0.1, 0.2, 0.8);
const v = pp.next().value;

Generator helper functions

In the Sync and Async sub-modules, there are a bunch of functions for working with generators or iterables.

For example:

import { Async, Sync } from "https://unpkg.com/ixfx/dist/generators.js"

for await (const v of Async.chunk(iterable, 5)) {
  // v is array of length 5, contains chunks of `iterable`
}

Here is a brief overview of available functions in these modules:

  • chunks: grab chunks of an iterable
  • concat: return the combined results from one or more iterables
  • dropWhile: ignore values that do not meet a predicate (opposite of filter)
  • equals: returns true if values in two iterables are the same
  • every: returns true if every value in iterable matches a predicate
  • fill: returns a replacement value for every value of the iterable
  • filter: returns items that match the predicate (opposite of `dropWhile``
  • forEach: run a function for each value
  • fromArray: creates an async generator from an array source
  • fromIterable: creates an async generator from an iterable/generator
  • map: returns value passed through a transform function
  • max/min: returns largest/smallest value seen
  • range: returns range of value seen
  • reduce: reduce values of iterable into one
  • slice: returns a section of an iterable
  • some: returns true and exits when a predicate function matches
  • takeWhile: returns items for which predicate returns true
  • toArray: copies contents of iterable to an array
  • unique: only yields items not yet seen
  • zip: combines the items from several iterables at same position

Only in Sync module

  • chunksOverlapping: like chunks, but start of a chunk is the same element as last of the previous chunk
  • find: returns the first value that matches a predicate. While some returns a boolean.
  • first/last: returns first/last value from an iterable
  • flatten: unnests values which are arrays
  • uniqueByValue: only yields unique values
  • yieldNumber: returns the numeric value from a generator