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
Where:
-
tree
recursively lists directories (in the example I limit it tosrc/
directory) -
grep
is used to filter results (single line) with provided pattern - we want only lines that containindex.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 ourindex.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;
Let's see what we got so far:
-
readFiles()
function executestree
command for providedpath
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 containsindex.js
-
findIndexFiles()
function filters array and returns new array with only entries containingindex.js
(internally usesisIndexFile()
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}`);
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}`);
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);
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
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' ]
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}`);
Top comments (0)