Skip to content

Process

Module Data.Process

There are times where several lines of code are used to process data. Doing a series of transformations to massage into some shape. For example:

const p = (input) => {
input = input.toUpperCase(); // Convert to upper-case
input = input.at(0) === `A`; // Set to true/false if first letter is an 'A'
return input; // Return true/false
}
p(`apple`); // _true_

This is a perfectly fine bit of code to write, but you’ll notice that the variable being manipulated - input - is repeated continually. This seems to violate the DRY principle (Don’t Repeat Yourself).

Since we assign it different data types, it’s also difficult at any point to be sure what data type it is. An alternative is to use intermediate variables:

const p = (input) => {
const toUpper = input.toUpperCase();
const isFirstLetterA = toUpper.at(0) === `A`;
return isFirstLetterA;
}
p(`apple`); // _true_

The advantage of this approach is we have more literate code, and we (or our code editor) can be more sure of what each data type is. However it seems wasteful to have to come up with names for all the steps.

We’d really just like to articulate the steps of each processing.

One of the patterns ixfx offers for doing this is Process.flow , which creates a flow of operators (up to 5 are supported). An operator is just a function that takes a value and returns a value.

The flow is encapsulated in a function that accepts an input value and returns an output.

import { Process } from 'https://unpkg.com/ixfx/dist/data.js';
// Create a flow that first converts to upper case, and then
// returns true/false if first letter is a capital A
const p = Process.flow(
value => value.toUpperCase(),
value => value.at(0) === 'A')
);
p('apple'); // _true_

This may not seem too much of an advantage over the original. We still have value repeated, for example.

However we’re now composing a series of operators as an array, rather than writing lines of code. This allows us to dynamically construct and manipulate flows at run time without needing hard-coded logic.

Another advantage is that operators can be defined, well-tested and re-used instead of ‘reinventing the wheel’.

Operators

A few pre-defined operators are available, see the API docs for full details

Math

Etc

  • Process.rank - returns the ‘best’ value seen so far based on a provided ranking function. This is like min/max but works for objects.

Temporal

Logic

Examples

The flow can produce effects, for example:

import { Process } from 'https://unpkg.com/ixfx/dist/data.js';
const p = Process.flow(
// Only ever output the largest-seen value
Process.max(),
// If value has not changed, output _undefined_
Process.seenLastToUndefined(),
// Only do something if not undefined,
// ie. we have a new max value
Process.ifNotUndefined(v => {
console.log(`v:`, v);
})
);
p(100); // Prints 'v:100'
p(90); // Nothing happens, max value has not changed
p(110); // Prints 'v:110'

If you want to process data from an event, define it once, and then use it within the event handler:

const coordFlow = Process.flow(
value => ({ x: value.x, y: value.y }), // Pluck just x,y
value => Points.divide(value, window.innerWidth, window.innerHeight) // Make relative to viewport
)
document.addEventListener(`pointermove`, event => {
const relativePoint = coordFlow(event);
// Gets a relative { x, y }
})

Likewise if you wanted to process data from an iterable or generator, use it within a for of loop or similar. This technique allows us to share the same data processing flow in different parts of the code.

Ternary

Operators can be added conditionally using the ternary operator.

import { Process } from 'https://unpkg.com/ixfx/dist/data.js';
const p = Process.flow(
value => value.toUpperCase(),
// If seconds past the minute...
new Date().getSeconds() < 30 ?
value => value.at(0) === 'A' : // ...is 0-29
value => value.at(0) === 'B' // ...or 30-59
);
// Sometime we'll check for 'A', sometimes 'B', depending on time
p('apple');