DEV Community

Cover image for JavaScript - Writing Functional Flows with Custom Pipes
Joshua Newell Diehl
Joshua Newell Diehl

Posted on

JavaScript - Writing Functional Flows with Custom Pipes


A very simple coding problem is often encountered early on in one's journey through programming, as it is a precursor to more interesting problems. It usually reads something like this:

Given an alphanumeric input of type {string},
write a function that returns a string in the reverse order

We'll use this familiar problem as the contrived basis for demonstrating the awesome possibilities of functional pipes.


Now, some may well have gotten cozy with the use of slick libraries like ramda and lodash, but perhaps fewer have a solid understanding of the mechanisms used.

The star player

We'll start by taking a patient look at the custom JavaScript pipe implementation that we'll use in the examples to come.

I've also included a more traditional syntax for all the mustache heralds out there (let us know in the comments what syntax you prefer!):

const pipe = 
     (...functions) =>
     (val) =>
        functions.reduce((prev, func) => func(prev), val);

// 
// OR
//

function pipeB(...functions) {
     return function(val) {
          return functions.reduce((prev, func) => {
               return func(val)
          }, val)
     }
};
Enter fullscreen mode Exit fullscreen mode

Those of us accustomed to writing Object-Oriented programs may find this syntax foreign. It uses functional techniques like currying and closures that will allow us to wrap, or decorate, our other functions.

The key operation, though, is the reduce that we call on our functions.

Let's break this down a bit.

We can deduce that:

  • The pipe accepts a series of functions and returns a function.
  • That function accepts a value and returns a reduce function that we call on our series of functions.
  • Every time our reducer iterates, one of our functions consumes the value and passes its output to the next function

What does all that do for us, then?
So, every one of the functions that we supply to the pipe will consume our value in a step-by-step sequence, one after the other. Here's where the concept of a pipe and it's semantic exactness begins to wax into full shape.

Imagining the purpose and form of a series of interconnected pipes, we can lay out a few functions with the expectation that they will be similarly pieced together:


const splitString = (string) => string.split("");
const reverseArray = (splitString) => splitString.reverse();
const joinArray = (reversedArray) => reversedArray.join(""); 
Enter fullscreen mode Exit fullscreen mode

Now let's create a custom flow with the pieces of our pipe:

const reverseStrFlow = pipe(splitString, reverseArray, joinArray);
Enter fullscreen mode Exit fullscreen mode

The result here is a new function that accepts a value, so consuming our input will look like this:

const reversedString = reverseStrFlow(stringToReverse);
Enter fullscreen mode Exit fullscreen mode

Finally, here is our code all together with example i/o:

const pipe = 
     (...functions) =>
     (val) =>
        functions.reduce((prev, func) => func(prev), val);

const splitString = (string) => string.split("");
const reverseArray = (splitString) => splitString.reverse();
const joinArray = (reversedArray) => reversedArray.join("");

const reverseStrFlow = pipe(splitString, reverseArray, joinArray);

const testString = "peloponnesian pumpkin";

const reversedString = reverseStrFlow(testString);
// expected output: "nikpmup naisennopolep"
Enter fullscreen mode Exit fullscreen mode

For the record, one would rarely find meaningful need to write this much code for a problem slayable by a sexy one-liner:
const reverseString = (string) => string.split("").reverse().join("")
I only hope to diminish the what in order to clarify the how:

By flowing our value through a pipe made of functional pieces!


A key point in beginning to imagine the usefulness of this technique is to remember that we can create any number of custom pipes with any number of functional pieces that we need. This makes the composition of complex operations possible with less code.
The benefits of the compositional style are numerous:

Reusability

Building in pieces allows for many possible unique combinations

Testability

Test for simple inputs and outputs

Ease of debugging

Smaller pieces, smaller problems

Avoid the pitfalls of inheritance

Ditch complex inheritance hierarchies that carry dead weight

...and many more


Now that we've tantalized your tasters with some functional flavors, let's wrap things up with a final working example, this time with syntax highlighting:

Full Code on Repl

Demonstration of making an abstract meal of chicken curry using pipe function


I'm honored if you've read this far ...

Keep going, keep growing, and don't forget to have fun!

Until next time,
Joshua

Top comments (0)

Some comments have been hidden by the post's author - find out more