flow is all about passing data between functions, which is an important part of functional programming – of any programming, really. Let's define and write our own version of flow and see how we can assemble complex operations from simple steps!

Why

Most coding is breaking a complex problem into smaller operations and ordering them correctly. Flow lets us break up imperative coding steps into discrete functions and then put them back together.

It can help to think of flow as creating a pipeline of operations. Your initial data goes in and a series of operations are performed on it, giving you the final result at the end.

While it can take getting used to, the change to the declarative style can reduce the amount of information you have to process to understand the intent of the code, which can help keep the intent and the implementation in sync.

Requirements

  • It must accept an array of functions.
  • It must return a function.
  • The return function must accept [at least] one argument.
  • The return function must call each function in the array, in order.
  • The argument must be passed to the first function in the array.
  • The output of the each function in the array will be passed as input to the subsequent function.
  • The last output should be returned.

Building It

Let's go by each requirement.

It must accept an array of functions.

const flow = (functions) => { };

It must return a function.

const flow = (functions) => () => {};

The return function must accept one argument.

const flow = (functions) => (input) => {};

The return function must call each function in the array, in order.

const flow = (functions) => (input) => {
  functions.map(fn => fn());
};

The argument must be passed to the first function in the array.

const flow = (functions) => (input) => {
  functions.map(fn => fn(input));
};

The output of the each function in the array will be passed as input to the subsequent function,

const flow = (functions) => (input) => {
  functions.reduce((lastInput, fn) => fn(lastInput), input);
};

The last output should be returned.

const flow = (functions) => (input) => functions
  .reduce((lastInput, fn) => fn(lastInput), input);

Coding Summary

That's it! We've written flow! It's actually a pretty straightforward function. Minimized it would be so small it would be difficult to read:

const flow=a=>b=>a.reduce((c,d)=>d(c),b);

Variations

Function Arguments or Array

If you want it to, flow can take arguments of functions rather than an array. Think of the difference between .apply() and .call(). This is a very small change.

// The rest operator creates the array
const flow = (...functions) => (input) => functions
  .reduce((lastInput, fn) => fn(lastInput), input);

You can even support both styles, accepting an array of arguments and using .flat() to eliminate nesting.

const flow = (...functions) => (input) => functions.flat()
  .reduce((lastInput, fn) => fn(lastInput), input);

Choosing one style or the other is usually recommended, but flexibility can be useful.

Multiple Input Arguments

While each step after the first will receive only the one return value of the previous function, the first function doesn't have to be limited to one argument. This does mean we treat the first function provided differently than the others, but it can be useful.

// Flow that accepts multiple inputs to first function.
const flow = ([
  first,
  ...others,
]) => (...inputs) => others.reduce(
  (lastInput, fn) => fn(lastInput),
  first(...inputs),
);

This variation assumes the function array rather than individual arguments. They could be combined together, though. We will leave that as an exercise if you are interested.

Testing An Example

Let's say we have this simple function:

const toFahrenheit = (celcius) => Math.max(-459.67, (celcius * 9 / 5) + 32);

You can see it does a few different things. Let's expand it out into multiple lines and add a few comments to help us understand what is happening.

Commented

function toFahrenheit (celcius) {
  // Conversion is three steps
  const multiplied = celcius * 9;
  const divided = multiplied / 5;
  const converted = divided + 32;
  // What about absolute zero? Set a minimum value
  const fahrenheit = Math.max(-459.67, converted);
  return fahrenheit;
}

We can pretty clearly see the four operations involved: multiplication, division, addition, and using Math.max() to create a minimum value.

Separating Functions

For functional programming, we need to break these steps into functions. We could use currying to make these more flexible, but for now we can create simple higher-order functions that accept single parameters and return functions until they have all the arguments they need. So instead of const add = (a, b) => a + b; we will use these:

const add = (additive) => (value) => value + additive;
const multiply = (multiple) => (value) => value * multiple;
const divideBy = (divisor) => (value) => value / divisor;
const limitMin = (maxValue) => (value) => Math.max(maxValue, value);

Putting it Back Together

Now that we have all the pieces, let's build it back up with flow(). Working from our commented version, each operation – as a higher-order function – is added in order to an array. We'll pass the first fixed value to each of the higher-order functions, so they each take a single value from the "previous step" in the pipeline.

Finally, flow() returns a new function ready to take our Celsius value!

const toFahrenheit = flow([
  multiply(9),
  divideBy(5),
  add(32),
  limitMin(-459.67),
]);

toFahrenheit(0);   // 32
toFahrenheit(37);  // 98.6
toFahrenheit(100); // 212

Implied Action

In the original function, we define the starting value celcius directly, but with flow() we don't. That's because flow returns a function that accepts the input. It can take some getting used to not having an explicit list of arguments when you compose functions this way.

Documentation like JSDoc or Typescript can help make it easier for the developer – and the tools in their editor – to understand the intent.

/**
 * @function toFahrenheit
 * @param {number} celcius
 * @returns {number} in Fahrenheit
 */
const toFahrenheit = flow([
  multiply(9),
  divideBy(5),
  add(32),
  limitMin(-459.67),
])
const toFahrenheit:(celcius:number)=>number = flow([
  multiply(9),
  divideBy(5),
  add(32),
  limitMin(-459.67),
]);

Compose vs. Flow

You may also hear about compose rather than flow. flow and compose are very similar; they are both composition operations. Flow tends to be easier to understand because the result of the first function flows like a pipeline into the second function. Compose is essentially the same but starting at the last function rather than the first. This style more closely matches the mathematical notation.

If we take the expression a(b(c(x), we would compose this like the order of functions written as compose([a, b, c])(x); or write the flow pipeline in the order they are executed, as flow([c, b, a])(x);.

The different to create compose rather than flow is just one method call: using .reduceRight() rather than .reduce().

// Flow: Left-to-right function execution
const flow = (functions) => (input) => functions
  .reduce((lastInput, fn) => fn(lastInput), input);

// Compose: Right-to-left function execution
const compose = (functions) => (input) => functions
  .reduceRight((lastInput, fn) => fn(lastInput), input);

Building Up

Once we have flow to create sets of operations, we can combine other operations to add or enhance the functionality.

Using a conditional function like doIf lets us assemble complex steps. I find this style makes code more like building blocks.

const sendPayment = flow([
  // Some payments have fees
  addTransactionFees,
  // Keep going if we have the money, or add an error.
  doIf(confirmSufficientFunds, addTimestamp, addErrorMessage),
  // Success or failure, record the transaction or attempt
  recordTransaction,
]);

These smaller functions with clear names provide a high-level view of what we expect this code to do. We don't know all the details of each step, and those details can change without changing the flow or our understanding of the code at this level. That ability to abstract details away is an important part of coding in any complex system.

Conclusion

flow() is an building block when using a functional programming style. You can use it to assemble simple functions into larger, complex operations while stripping away a lot of "noise" or boilerplate surrounding the meaningful content.

There are many variations on how flow can work. Is there a style you prefer?