Functional programming from the point of view of EcmaScript. Composition, Currying, Partial Application

Hello, Habr!



Today we continue our research on functional programming in the context of EcmaScript, the specification of which is based on JavaScript. In the previous article, we examined the basic concepts: pure functions, lambdas, the concept of immunity. Today we’ll talk about slightly more complex FP techniques: composition, currying and pure functions. The article is written in the style of “pseudo codreview”, i.e. we will solve a practical problem, while studying the concepts of phase transitions and refactor code to approximate the latter to the ideals of phase transitions.



So, let's begin!



Suppose we are faced with a task: to create a set of tools for working with palindromes.

PALINDROME

Male gender

A word or phrase that is read alike from left to right and from right to left.

"P. "I go with the sword of the judge" "

One of the possible implementations of this task could look like this:



function getPalindrom (str) { const regexp = /[\.,\/#!$%\^&\*;:{}=\-_`~()?\s]/g; str = str.replace(regexp, '').toLowerCase().split('').reverse().join(''); // -       ,       return str; } function isPalindrom (str) { const regexp = /[\.,\/#!$%\^&\*;:{}=\-_`~()?\s]/g; str = str.replace(regexp, '').toLowerCase(); return str === str.split('').reverse().join(''); }
      
      





Of course, this implementation works. We can expect that getPalindrom will work correctly if the api returns the correct data. A call to isPalindrom ('I'm going with a sword judge') will return true, and a call to isPalindrom ('not a palindrome') will return false. Is this implementation good in terms of ideals of functional programming? Definitely not good!



According to the definition of Pure Functions from this article :

Pure functions (PF) - always return a predicted result.

PF Properties:



The result of PF execution depends only on the arguments passed and the algorithm that implements PF

Do not use global values

Do not modify outside values ​​or passed arguments

Do not write data to files, databases or anywhere else

And what do we see in our example with palindromes?



Firstly, there is code duplication, i.e. the principle of DRY is violated. Secondly, the getPalindrom function accesses the database. Third, functions modify their arguments. Total, our functions are not clean.



Recall the definition: Functional programming is a way of writing code through compiling a set of functions.



We compose a set of functions for this task:



 const allNotWordSymbolsRegexpGlobal = () => /[\.,\/#!$%\^&\*;:{}=\-_~()?\s]/g;//(1) const replace = (regexp, replacement, str) => str.replace(regexp, replacement);//(2) const toLowerCase = str => str.toLowerCase();//(3) const stringReverse = str => str.split('').reverse().join('');//(4) const isStringsEqual = (strA, strB) => strA === strB;//(5)
      
      





In line 1, we declared the regular expression constant in functional form. This method of describing constants is often used in FP. In line 2, we encapsulated the String.prototype.replace method in the functional replace abstraction so that it (the replace call) matches the functional programming contract. On line 3, an abstraction for String.prototype.toLowerCase was created in the same way. In the 4th, they implemented a function that creates a new expanded string from the passed one. 5th checks for string equality.



Please note that our features are extremely clean! We talked about the benefits of pure functions in a previous article .



Now we need to implement a check to see if the string is a palindrome. A composition of functions will come to our aid.



The composition of functions is the union of two or more functions into a certain resulting function that implements the behavior of those combined in the desired algorithmic sequence.



The definition may seem complicated, but from a practical point of view it is fair.



We can do this:



 isStringsEqual(toLowerCase(replace(allNotWordSymbolsRegexpGlobal(), '', '    ')), stringReverse(toLowerCase(replace(allNotWordSymbolsRegexpGlobal(), '', '    '))));
      
      





or like this:



 const strA = toLowerCase(replace(allNotWordSymbolsRegexpGlobal(), '', '    ')); const strB = stringReverse(toLowerCase(replace(allNotWordSymbolsRegexpGlobal(), '', '    '))); console.log(isStringsEqual(strA, strB));
      
      





or enter another bunch of explanatory variables for each step of the implemented algorithm. Such code can often be seen on projects, and this is a typical example of composition — passing a call to one function as an argument to another. Nevertheless, as we see, in a situation when there are many functions, this approach is bad, because this code is not readable! So what now? Well, its functional programming, do we disagree?



In fact, as is usually the case in functional programming, we just need to write another function.



 const compose = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);
      
      





The compose function takes as arguments a list of functions that are executed, turns them into an array, stores it in a closure, and returns a function that expects an initial value. After the initial value is passed, the sequential execution of all functions from the fns array starts. The argument of the first function will be the initial value x passed, and the arguments of all subsequent ones will be the result of the previous one. So we can create compositions of any number of functions.



When creating functional compositions, it is very important to monitor the types of input parameters and return values ​​of each function so that there are no unexpected errors, because we pass the result of the previous function to the next.



However, already now we see problems with applying the composition technique to our code, because the function:



 const replace = (regexp, replacement, str) => str.replace(regexp, replacement);
      
      





expects to accept 3 input parameters, and we only send one to compose. Another FP technique, Currying, will help us solve this problem.



Currying is the conversion of a function from many arguments to a function from one argument.



Remember our add function from the first article?



 const add = (x,y) => x+y;
      
      





It can be curried like this:



 const add = x => y => x+y;
      
      





The function takes x and returns a lambda that expects y and performs the action.



Curry Benefits:





Now we transform our replace function so that it takes only one argument. Since we need the function to replace characters in the string with a previously known regular expression, we can create a partially applied function.



 const replaceAllNotWordSymbolsGlobal = replacement => str => replace(allNotWordSymbolsRegexpGlobal(), replacement, str);
      
      





As you can see, we fix one of the arguments with a constant. This is due to the fact that currying is actually a special case of partial use.



A partial application is wrapping a function with a wrapper that accepts fewer arguments than the function itself; the wrapper should return a function that takes the rest of the arguments.



In our case, we created the replaceAllNotWordSymbolsGlobal function, which is a partially used option for replace. It accepts replacement, stores it in a closure, and expects an input line for which it will call replace, and we fix the regexp argument with a constant.



Let's go back to the palindromes. Create a composition of functions for the palindrome timing:



 const processFormPalindrom = compose( replaceAllNotWordSymbolsGlobal(''), toLowerCase, stringReverse );
      
      





and the composition of functions for the line with which we will compare the potential palindrome:



 const processFormTestString = compose( replaceAllNotWordSymbolsGlobal(''), toLowerCase, );
      
      





Now remember what we said above:

a typical composition example is passing a call to one function as an argument to another
and write:



 const testString = '    ';//          , .. ,    ,  ,   -   ,    const isPalindrom = isStringsEqual(processFormPalindrom(testString), processFormTestString(testString));
      
      





Here we have a working and good looking solution:



 const allNotWordSymbolsRegexpGlobal = () => /[\.,\/#!$%\^&\*;:{}=\-_~()?\s]/g; const replace = (regexp, replacement, str) => str.replace(regexp, replacement); const toLowerCase = str => str.toLowerCase(); const stringReverse = str => str.split('').reverse().join(''); const isStringsEqual = (strA, strB) => strA === strB; const replaceAllNotWordSymbolsGlobal = replacement => str => replace(allNotWordSymbolsRegexpGlobal(), replacement, str); const compose = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x); const processFormPalindrom = compose( replaceAllNotWordSymbolsGlobal(''), toLowerCase, stringReverse ); const processFormTestString = compose( replaceAllNotWordSymbolsGlobal(''), toLowerCase, ); const testString = '    '; const isPalindrom = isStringsEqual(processFormPalindrom(testString), processFormTestString(testString));
      
      





However, we do not want to do currying every time or to create partially applied functions with our hands. Of course we don’t want it, programmers are lazy people. Therefore, as it usually happens in FP, we will write a couple more functions:



 const curry = fn => (...args) => { if (fn.length > args.length) { const f = fn.bind(null, ...args); return curry(f); } else { return fn(...args) } }
      
      





The curry function takes a function to be curried, stores it in a closure, and returns a lambda. The lambda expects the rest of the arguments to the function. Each time an argument is received, it checks to see if all declared arguments are accepted. If accepted, then the function is called and its result is returned. If not, the function is curried again.



We can also create a partially applied function to replace the regular expression we need with an empty string:



 const replaceAllNotWordSymbolsToEmpltyGlobal = curry(replace)(allNotWordSymbolsRegexpGlobal(), '');
      
      





Everything seems to be fine, but we are perfectionists and we don’t like too many brackets, we would like even better, so we’ll write another function or maybe two:



 const party = (fn, x) => (...args) => fn(x, ...args);
      
      





This is an abstraction implementation for creating partial applied functions. It takes a function and the first argument, returns a lambda that expects the rest and executes the function.



Now we rewrite party so that we can create a partially applied function of several arguments:



 const party = (fn, ...args) => (...rest) => fn(...args.concat(rest));
      
      





It is worth noting separately that functions curried in this way can be called with any number of arguments less than declared (fn.length).



 const sum = (a,b,c,d) => a+b+c+d; const fn = curry(sum); const r1 = fn(1,2,3,4);//,   const r2 = fn(1, 2, 3)(4);//       const r3 = fn(1, 2)(3)(4); const r4 = fn(1)(2)(3)(4); const r5 = fn(1)(2, 3, 4); const r6 = fn(1)(2)(3, 4); const r7 = fn(1, 2)(3, 4);
      
      





Let's get back to our palindromes. We can rewrite our replaceAllNotWordSymbolsToEmpltyGlobal without extra brackets:



 const replaceAllNotWordSymbolsToEmpltyGlobal = party(replace,allNotWordSymbolsRegexpGlobal(), '');
      
      





Let's look at the whole code:



 //    -       const allNotWordSymbolsRegexpGlobal = () => /[\.,\/#!$%\^&\*;:{}=\-_~()?\s]/g; const replace = (regexp, replacement, str) => str.replace(regexp, replacement); const toLowerCase = str => str.toLowerCase(); const stringReverse = str => str.split('').reverse().join(''); const isStringsEqual = (strA, strB) => strA === strB; //       const testString = '    '; //           -    rambda.js const compose = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x); const curry = fn => (...args) => { if (fn.length > args.length) { const f = fn.bind(null, ...args); return curry(f); } else { return fn(...args) } } const party = (fn, ...args) => (...rest) => fn(...args.concat(rest)); //       const replaceAllNotWordSymbolsToEmpltyGlobal = party(replace,allNotWordSymbolsRegexpGlobal(), ''); const processFormPalindrom = compose( replaceAllNotWordSymbolsToEmpltyGlobal, toLowerCase, stringReverse ); const processFormTestString = compose( replaceAllNotWordSymbolsToEmpltyGlobal, toLowerCase, ); const checkPalindrom = testString => isStringsEqual(processFormPalindrom(testString), processFormTestString(testString));
      
      





It looks great, but what if it’s not a string for us, but an array will come? Therefore, we add another function:



 const map = fn => (...args) => args.map(fn);
      
      





Now if we have an array for testing for palindromes, then:



 const palindroms = ['    ','   ','   '. ' '] map(checkPalindrom )(...palindroms ); // [true, true, true, false]  
      
      





That's how we solved the task by writing feature sets. Pay attention to the pointless style of writing code - it is a litmus test of functional purity.



Now a little more theory. Using currying, do not forget that each time you curry a function, you create a new one, i.e. select a memory cell for it. It is important to monitor this to avoid leaks.



Functional libraries like ramda.js have compose and pipe functions. compose implements the composition algorithm from right to left, and pipe from left to right. Our compose function is an analogue of pipe from ramda. There are two different composition functions in the library since composition from right to left and left to right are two different contracts of functional programming. If one of the readers finds an article that describes all existing contracts of the FP, then share it in the comments, I will read it with pleasure and put a plus to the comment!



The number of formal parameters of a function is called arity . this is also an important definition from the point of view of the theory of phase transitions.



Conclusion



In the framework of this article, we examined such functional programming techniques as composition, currying and partial application. Of course, on real projects, you will use ready-made libraries with these tools, but as part of the article, I implemented everything on native JS so that readers with perhaps not very much experience in the FP can understand how these techniques work under the hood.



I also consciously chose the method of narration - pseudo codreview, in order to illustrate my logic of achieving functional purity in the code.



By the way, you can continue the development of this module of working with palindromes and develop its ideas, for example, upload lines by api, convert to letter sets and send to the server where the line will be generated by the palindrome and much more ... At your discretion.



It would also be nice to get rid of duplication in the processes of these lines:



  replaceAllNotWordSymbolsToEmpltyGlobal, toLowerCase,
      
      





In general, you can and should improve the code all the time!



Until future articles.



All Articles