I love the pipe function. It's insanely useful in function composition and data transformation workflows where you send data through a long list of functions.
I wont go into the details and intricacies of the pipe function but the gist is best described with this example:
Suppose you want to do all these steps in sequence:
- take a number
- add 1 to the result
- then double that result
- then square the result of doubling
- then find the square-root of the previous result
- then halve it
- and finally decrement it
The example is trite (and you end up with the same number) but if you had a simple pipe function, you could write it like so:
// the input number const doSteps = (number) => pipe( // assume that all these named functions are defined somewhere inc1, // a => a + 1 double, // a => a * 2 square, // a => a * a root, // basically Math.sqrt halve, // a => a / 2 dec1 // a => a - 1 )(number); doSteps(2); // 2
However, real-life scenarios are hardly as ideal or simple.
You deal with functions that could potentially throw an error and that kind of throws a spanner in the works. Unless you are okay with your pipe composition to throw (because one of the functions in the pipe did), it's not very neat. And I don't like runtime exceptions in my app so I use this handy-little exception-free pipe
.
const safeWrapper = (func, value) => { if (value.err) { return { err: value.err }; } if (value.data) { try { let res = func(value.data); return { data: res }; } catch (e) { return { err: e }; } } }; const pipe = (...fns) => (initialInput) => { return fns.reduce( (acc, fn) => { return safeWrapper(fn, acc); }, { data: initialInput } ); };
What's happening here is that I'm not calling the functions in the pipe and using their results directly. Instead, I'm wrapping them – sort of like a Faraday's box – so that if the functions throw an error, they are contained.
A small change here is that the final output is of the form:
{ data: any | undefined, err: Err | undefined}
As an example:
let inc1 = (a) => a + 1; let double = (a) => a * 2; let square = (a) => a * a; let root = Math.sqrt; let halve = (a) => a / 2; let dec1 = (a) => a - 1; pipe(inc1, double, square, root, halve, dec1)(2); // { data: 2 };
But if one or any of the functions throw:
let err = new Error('error when squaring'); let inc1 = (a) => a + 1; let double = (a) => a * 2; let square = (a) => { throw err; // instead of doing the job, we throw an error }; let root = Math.sqrt; let halve = (a) => a / 2; let dec1 = (a) => a - 1; pipe(inc1, double, square, root, halve, dec1)(2); // { err: Error('error when squaring') };
The first function to throw will (kind of) "short-circuit" the whole operation and that will be the error returned.
Top comments (0)