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?
Top comments (0)