DEV Community

Samuel Rouse
Samuel Rouse

Posted on

flow: JavaScript Functional Programming

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) => { };
Enter fullscreen mode Exit fullscreen mode

It must return a function.

const flow = (functions) => () => {};
Enter fullscreen mode Exit fullscreen mode

The return function must accept one argument.

const flow = (functions) => (input) => {};
Enter fullscreen mode Exit fullscreen mode

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

const flow = (functions) => (input) => {
  functions.map(fn => fn());
};
Enter fullscreen mode Exit fullscreen mode

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

const flow = (functions) => (input) => {
  functions.map(fn => fn(input));
};
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

The last output should be returned.

const flow = (functions) => (input) => functions
  .reduce((lastInput, fn) => fn(lastInput), input);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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),
);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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),
])
Enter fullscreen mode Exit fullscreen mode
const toFahrenheit:(celcius:number)=>number = flow([
  multiply(9),
  divideBy(5),
  add(32),
  limitMin(-459.67),
]);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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,
]);
Enter fullscreen mode Exit fullscreen mode

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?

Image of PulumiUP 2025

Explore What’s Next in DevOps, IaC, and Security

Join us for demos, and learn trends, best practices, and lessons learned in Platform Engineering & DevOps, Cloud and IaC, and Security.

Save Your Spot

Top comments (0)

Image of Stellar post

🚀 Stellar Dev Diaries Series: Episode 1 is LIVE!

Ever wondered what it takes to build a web3 startup from scratch? In the Stellar Dev Diaries series, we follow the journey of a team of developers building on the Stellar Network as they go from hackathon win to getting funded and launching on mainnet.

Read more

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, valued within the supportive DEV Community. Coders of every background are welcome to join in and add to our collective wisdom.

A sincere "thank you" often brightens someone’s day. Share your gratitude in the comments below!

On DEV, the act of sharing knowledge eases our journey and fortifies our community ties. Found value in this? A quick thank you to the author can make a significant impact.

Okay