DEV Community

webit
webit

Posted on

Composing functions in JavaScript

Recently I read an article about using Pipeline style in JavaScript.
An article described how to pipe functions together so data flows through all of them.
What I've missed in this article was functional programming taste.
Let's go a step further and add some FP flavor.

Using pipelines in *nix shell

Imagine *nix command line where we want to find all index.js files in a certain directory. When we will get a list of files we would like to count them.
Let's say we got source code placed inside src/.
It's a trivial example but explains how we can use pipe commands (using |) in *nix shell to pass data through them.

To achieve what we want we have to execute the following command:

tree src/ | grep index.js | wc -l
Enter fullscreen mode Exit fullscreen mode

Where:

  • tree recursively lists directories (in the example I limit it to src/ directory)
  • grep is used to filter results (single line) with provided pattern - we want only lines that contain index.js
  • wc (word count) returns newline count, word count, and byte count. Used with -l returns only the first value so the number of times our index.js was found

Example output from the above command can be any number, in my case, it's 26.

What we see here is how data is passed from one command to another. The first command works on input data and returns data to the second one. And so on until we reach the end - then data returned by the last command is displayed.

Using pipelines in JavaScript

We can achieve a similar thing in JavaScript.
First, let's build a function that serves for certain purpose mimicking shell commands.

// node's execSync allows us to execute shell command
const { execSync } = require("child_process"); 

// readFiles = String => Buffer
const readFiles = (path = "") => execSync(`tree ${path}`);
// bufferToString = Buffer => String
const bufferToString = buffer => buffer.toString();
// makeFilesList = String => Array
const makeFilesList = files => files.split("\n");
// isIndex = String => Boolean
const isIndexFile = file => file.indexOf("index.js") > 0;
// findIndexFiles = Array => Array
const findIndexFiles = files => files.filter(isIndexFile);
// countIndexFiles = Array => Number
const countIndexFiles = files => files.length;
Enter fullscreen mode Exit fullscreen mode

Let's see what we got so far:

  • readFiles() function executes tree command for provided path or in location where our JS file was executed. Function returns Buffer
  • bufferToString() function converts Buffer data to String
  • makeFilesList() function converts received string to array making each line of text separate array element
  • isIndexFile() function check if provided text contains index.js
  • findIndexFiles() function filters array and returns new array with only entries containing index.js (internally uses isIndexFile() function)
  • countIndexFiles() function simply counts elements in provided array

Now we got all the pieces to do our JavaScript implementation. But how to do that?
We will use function composition and the key here is using unary functions.

Function composition

Unary functions are functions that receive exactly one parameter.

Since they accept one argument we can connect them creating a new function. This technique is called function composition. Then data returned by one function is used as an input for another one.

We can use compose function that you can find in the popular functional programming library Ramda.
Let's see how to do that...

// returns function that accepts path parameter passed to readFiles()
const countIndexFiles = R.compose(
  countIndexFiles,
  findIndexFiles,
  makeFilesList,
  bufferToString,
  readFiles);

const countIndexes = countIndexFiles("src/");

console.log(`Number of index.js files found: ${countIndexes}`);
Enter fullscreen mode Exit fullscreen mode

Note: we can actually compose functions without even using compose function (but I think this is less readable):

const countIndexes = countIndexFiles(findIndexFiles(makeFilesList(bufferToString(readFiles("src/")))));

console.log(`Number of index.js files found: ${countIndexes}`);
Enter fullscreen mode Exit fullscreen mode

As you can see function composition allows us to join functions and don't worry about handling data between them. Here's what we have to do without using composition:

const filesBuf = readFiles("src/");
const filesStr = bufferToString(filesBuf);
const filesList = makeFilesList(filesStr);
const indexFiles = findIndexFiles(filesList);
const countIndexes = countIndexFiles(indexFiles);
Enter fullscreen mode Exit fullscreen mode

Compose vs pipe

As you might have noticed when using compose we need to pass functions in the opposite order they are used (bottom-to-top).
It's easier to read them in top-to-bottom order. That's the place where pipe comes in. It does the same compose does but accepts functions in reverse order.

// even though not takes functions list in reverse order 
// it still accepts path parameter passed to readFiles()
const countIndexFiles = R.pipe(
  readFiles,
  bufferToString,
  makeFilesList,
  findIndexFiles,
  countIndexFiles);

const countIndexes = countIndexFiles("src/");

console.log(`Number of index.js files found: ${countIndexes}`); // same result as before
Enter fullscreen mode Exit fullscreen mode

It depends just on us which one method we will use - compose or pipe.
Try to use one you (and your colleagues) feel better with.

Bonus: use full power Ramda gives you

We can use other Ramda methods to even more shorten our code. This is because all Ramda functions are curried by default and come with the "data last" style.
This means we can configure them before providing data. For example R.split creates new function that splits text by provided separator. But it waits for a text to be passed:

const ipAddress = "127.0.0.1";
const ipAddressParts = R.split("."); // -> function accepting string

console.log(ipAddressParts(ipAddress)); // -> [ '127', '0', '0', '1' ]
Enter fullscreen mode Exit fullscreen mode

Enough theory 👨‍🎓
Let's see how our code could look like in final (more FP style) form:

const { execSync } = require("child_process");
const R = require("ramda");

// readFiles = String => Buffer
const readFiles = (path = "") => execSync(`tree ${path}`);
// bufferToString = Buffer => String
const bufferToString = buffer => buffer.toString();
// isIndex = String => Boolean
const isIndexFile = file => file.indexOf("index.js") > 0;

const countIndexFiles = R.pipe(
  readFiles,
  bufferToString,
  R.split("\n"),
  R.filter(isIndexFile),
  R.length);

const countIndexes = countIndexFiles("src/");

console.log(`Number of index.js files found: ${countIndexes}`);
Enter fullscreen mode Exit fullscreen mode

Top comments (0)