Recently I read an article about usingPipeline 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 allindex.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 insidesrc/
.
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's26
.
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 commandconst{execSync}=require("child_process");// readFiles = String => BufferconstreadFiles=(path="")=>execSync(`tree${path}`);// bufferToString = Buffer => StringconstbufferToString=buffer=>buffer.toString();// makeFilesList = String => ArrayconstmakeFilesList=files=>files.split("\n");// isIndex = String => BooleanconstisIndexFile=file=>file.indexOf("index.js")>0;// findIndexFiles = Array => ArrayconstfindIndexFiles=files=>files.filter(isIndexFile);// countIndexFiles = Array => NumberconstcountIndexFiles=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 BufferbufferToString()
function converts Buffer data to StringmakeFilesList()
function converts received string to array making each line of text separate array elementisIndexFile()
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 usefunction composition and the key here is usingunary functions.
Function composition
Unary functions are functions that receiveexactly one parameter.
Since they accept one argument we can connect them creating a new function. This technique is calledfunction composition. Then data returned by one function is used as an input for another one.
We can usecompose
function that you can find in the popular functional programming libraryRamda.
Let's see how to do that...
// returns function that accepts path parameter passed to readFiles()constcountIndexFiles=R.compose(countIndexFiles,findIndexFiles,makeFilesList,bufferToString,readFiles);constcountIndexes=countIndexFiles("src/");console.log(`Number of index.js files found:${countIndexes}`);
Note: we can actually compose functions without even usingcompose
function (but I think this is less readable):
constcountIndexes=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:
constfilesBuf=readFiles("src/");constfilesStr=bufferToString(filesBuf);constfilesList=makeFilesList(filesStr);constindexFiles=findIndexFiles(filesList);constcountIndexes=countIndexFiles(indexFiles);
Compose vs pipe
As you might have noticed when usingcompose
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 wherepipe
comes in. It does the samecompose
does but accepts functions in reverse order.
// even though not takes functions list in reverse order// it still accepts path parameter passed to readFiles()constcountIndexFiles=R.pipe(readFiles,bufferToString,makeFilesList,findIndexFiles,countIndexFiles);constcountIndexes=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
orpipe
.
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 arecurried by default and come with the "data last" style.
This means we can configure them before providing data. For exampleR.split
creates new function that splits text by provided separator. But it waits for a text to be passed:
constipAddress="127.0.0.1";constipAddressParts=R.split(".");// -> function accepting stringconsole.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");constR=require("ramda");// readFiles = String => BufferconstreadFiles=(path="")=>execSync(`tree${path}`);// bufferToString = Buffer => StringconstbufferToString=buffer=>buffer.toString();// isIndex = String => BooleanconstisIndexFile=file=>file.indexOf("index.js")>0;constcountIndexFiles=R.pipe(readFiles,bufferToString,R.split("\n"),R.filter(isIndexFile),R.length);constcountIndexes=countIndexFiles("src/");console.log(`Number of index.js files found:${countIndexes}`);
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse