📜 ⬆️ ⬇️

Functional JavaScript programming with practical examples

image


Functional programming (FP) can improve your approach to writing code. But the OP is not easy to master. Many articles and guides do not pay attention to such details as monads (Monads), applicativeness (Applicative), etc., do not give as illustrations practical examples that could help us in the daily use of powerful FP techniques. I decided to correct this omission.


I want to emphasize: the article focuses on why the feature X is needed, and not on what the feature X is.


Functional programming


FP is a writing style for programs that simply combines a set of functions. In particular, the FP implies wrapping in the function of almost the entire row. You have to write many small reusable functions and call them one after another to get a result like (func1.func2.func3) or combinations of the type func1 (func2 (func3 ())) .


But to really write programs in this style, functions must follow certain rules and solve some problems.


AF issues


If everything can be done by combining a set of functions, then ...


  1. How to handle an if-else condition? (Hint: Monad Either)
  2. How to handle Null exceptions? (Hint: Maybe monad)
  3. How to make sure that the functions are really reusable and can be used everywhere? (Hint: pure functions, referential transparency)
  4. How to make sure that the data passed to us in the function is not changed and can be used in other places? (Hint: Pure functions, immutability)
  5. If a function takes several values, but when chaining you can transfer only one function, then how do we make this function a part of a chain? (Hint: Currying and Higher Order Functions)
  6. And much more <add your question here>.

OP solution


To solve all these problems, fully functional languages ​​like Haskell out of the box provide different tools and mathematical concepts, such as monads, functors, etc. JavaScript out of the box does not provide such an abundance of tools, but, fortunately, it has a sufficient set of OP properties, allow to write libraries.


Fantasy Land Specifications and OP Libraries


If libraries want to provide features such as functors, monads, etc., then they need to implement functions / classes that meet certain specifications so that the capabilities provided are the same as in languages ​​like Haskell.


A prime example is the Fantasy Land specifications , which explain how each JS function / class should behave.


image


The illustration shows all the specifications and their dependencies. Specifications are essentially laws; they are similar to interfaces in Java. From the point of view of the JS specification, we can consider the classes or constructors functions that implement certain methods (such as map, of, chain , etc.) according to the specification.


For example:


A JS class is a functor (Functor) if it implements the map method. And the method should work as prescribed by the specification (the explanation is simplified, the rules are actually more).


The JS class is the Apply (Apply Functor) functor if it implements the map and ap functions in accordance with the specification.


The JS class is a monad (Monad Functor) if it implements the requirements of Functor, Apply, Applicative, Chain and Monad itself (according to the dependency chain).


Note: the dependency may look like inheritance, but not necessarily. For example, a monad implements both specifications — Applicative and Chain (in addition to the rest).


Libraries Compliant with Fantasy Land Specs


There are several libraries that implement the FL specifications. For example: monet.js , barely-functional , folktalejs , ramda-fantasy (based on Ramda), immutable-ext (based on ImmutableJS), Fluture , etc.


Which libraries should I use?


Libraries like lodash-fp and ramdajs will only allow you to start writing in the style of the OP. But they do not provide functions for using key mathematical concepts such as monads, functors, or the Foldable to solve real problems.


So, I would also recommend choosing one of the libraries that use the FL specifications: monet.js , barely-functional , folktalejs , ramda-fantasy (based on Ramda), immutable-ext (based on ImmutableJS), Fluture , etc.


Note: I use ramdajs and ramda-fantasy .


So, we got an idea of ​​the basics, now let's move on to practical examples and explore the various possibilities and techniques of AF.


Example 1. Work with checks on Null


The section discusses: functors, monads, Maybe monads, currying


Application: we want to show different home pages depending on the user preference of the preferred language . You need to write getUrlForUser , which returns the corresponding URL from the list of URLs (indexURLs) for the user's (joeUser) preferred language (Spanish).


image


Problem: language cannot be null. And the user also can not be null (not logged in). The language may not be in our indexURL list. Therefore, we need to take care of numerous nulls or undefined.


//TODO        const getUrlForUser = (user) => { //todo } //  let joeUser = { name: 'joe', email: 'joe@example.com', prefs: { languages: { primary: 'sp', secondary: 'en' } } }; //  indexURL'    let indexURLs = { 'en': 'http://mysite.com/en', // 'sp': 'http://mysite.com/sp', // 'jp': 'http://mysite.com/jp' // } //apply url to window.location const showIndexPage = (url) => { window.location = url }; 

Solution (imperative versus functional):


Do not worry if the OP version looks difficult to understand. Further in this article we will analyze it step by step.


 // : //  if-else    null;    indexURL'; «» URL'       const getUrlForUser = (user) => { if (user == null) { //  return indexURLs['en']; //    } if (user.prefs.languages.primary && user.prefs.languages.primary != 'undefined') { if (indexURLs[user.prefs.languages.primary]) {//   ,   indexURLs[user.prefs.languages.primary]; } else { return indexURLs['en']; } } } // showIndexPage(getUrlForUser(joeUser)); // : //(   ,   ,  ) // -: ,  Maybe  Currying const R = require('ramda'); const prop = R.prop; const path = R.path; const curry = R.curry; const Maybe = require('ramda-fantasy').Maybe; const getURLForUser = (user) => { return Maybe(user)//    Maybe .map(path(['prefs', 'languages', 'primary'])) // Ramda     .chain(maybeGetUrl); //   maybeGetUrl   URL   null } const maybeGetUrl = R.curry(function(allUrls, language) {//      return Maybe(allUrls[language]);//  (url | null) })(indexURLs);//     indexURLs function boot(user, defaultURL) { showIndexPage(getURLForUser(user).getOrElse(defaultURL)); } boot(joeUser, 'http://site.com/en'); //'http://site.com/sp' 

Let's first analyze the FP concepts and techniques used in this solution.


Functors


Any class (or constructor function) or data type that stores a value and implements the map method is called a functor.


For example: an array is a functor because it can store values ​​and has a map method that allows us to apply (map) a function to stored values.


 const add1 = (a) => a+1; let myArray = new Array(1, 2, 3, 4); //  myArray.map(add1) // -> [2,3,4,5] //  

We write our own functor - MyFunctor. This is just a JS class (constructor function) that stores a value and implements the map method. This method applies the function to the stored value, and then creates a new Myfunctor from the result and returns it.


 const add1 = (a) => a + 1; class MyFunctor { //  constructor(value) { this.val = value; } map(fn) { //   this.val +   Myfunctor return new Myfunctor(fn(this.val)); } } //temp —   ,   1 let temp = new MyFunctor(1); temp.map(add1) //-> temp    (map) "add1" 

PS Functors must implement other specifications ( Fantasy-land ) in addition to the map, but we will not touch on this here.


Monads


Monads are also functors, that is, they have a map method. But they realize not only him. If you look at the dependency scheme again, you will see that monads must implement different functions from different specifications, for example, Apply ( ap method), Applicative ( ap and of methods) and Chain ( chain method).


image


Simplified explanation . In JS, monads are constructor classes or functions that store some data and implement map, ap, of, and chain methods that do something with stored data in accordance with specifications.


Here is a sample implementation for you to understand the inner structure of monads.


 // —   class Monad { constructor(val) { this.__value = val; } static of(val) {//Monad.of ,  new Monad(val) return new Monad(val); }; map(f) {// ,    ! return Monad.of(f(this.__value)); }; join() { //       return this.__value; }; chain(f) {// ,  (map),     return this.map(f).join(); }; ap(someOtherMonad) {//      return someOtherMonad.map(this.__value); } } 

Monads are not commonly used, but more specific and more useful. For example, Maybe or Either.


Monad Maybe


The Maybe monad is a class that implements a monad specification. But the feature of the monad is that it correctly processes null or undefined values.


In particular, if the stored data is null or undefined, then the map function does not perform this function at all, therefore there are no problems with null and undefined. Such a monad is used in situations where you have to deal with null values.


The code below shows the ramda-fantasy implementation of the Maybe monad. Depending on the value, it creates an instance of one of two different subclasses — Just or Nothing (the value is either useful or null / undefined).


Although the Just and Nothing methods are the same (map, orElse, etc.), Just does something, and Nothing does nothing.


Pay particular attention to the map and orElse methods in this code:


 //    Maybe   ramda-fantasy //    : https://github.com/ramda/ramda-fantasy/blob/master/src/Maybe.js function Maybe(x) { //<--  ,   Maybe  Just  Nothing return x == null ? _nothing : Maybe.Just(x); } function Just(x) { this.value = x; } util.extend(Just, Maybe); Just.prototype.isJust = true; Just.prototype.isNothing = false; function Nothing() {} util.extend(Nothing, Maybe); Nothing.prototype.isNothing = true; Nothing.prototype.isJust = false; var _nothing = new Nothing(); Maybe.Nothing = function() { return _nothing; }; Maybe.Just = function(x) { return new Just(x); }; Maybe.of = Maybe.Just; Maybe.prototype.of = Maybe.Just; //  Just.prototype.map = function(f) { // map,  Just  ,   Just    return this.of(f(this.value)); }; Nothing.prototype.map = util.returnThis; // <--  map,  Nothing    Just.prototype.getOrElse = function() { return this.value; }; Nothing.prototype.getOrElse = function(a) { return a; }; module.exports = Maybe; 

Let's see how you can use the Maybe monad to work with checks on null.


Let's go step by step:


  1. If there is any object that can be null or have null-properties, then create from it an object-monad.
  2. Use libraries like ramdajs that use Maybe to access the value from inside and outside the monad.
  3. Provide a default value if the real value is null (i.e., we process null errors in advance).

 // 1. ... if (user == null) { //  return indexURLs['en']; //    } //: Maybe(user) // Maybe({userObj})  Maybe(null).      Maybe // 2. ... if (user.prefs.languages.primary && user.prefs.languages.primary != 'undefined') { if (indexURLs[user.prefs.languages.primary]) {//   , return indexURLs[user.prefs.languages.primary]; //: //,      Maybe,  map.path  Ramda: <userMaybe>.map(path(['prefs', 'languages', 'primary'])) // 3. ... return indexURLs['en']; //hardcoded default values //: // Maybe-   orElse  getOrElse,     ,     <userMayBe>.getOrElse('http://site.com/en') 

Currying (helps to work with global data and multiparameter functions)


The section covers: pure functions (Pure functions) and composition (Composition)


If we want to make chains of functions - func1.func2.func3 or (func1 (func2 (func3 ())), then each of them can take only one parameter. For example, if func2 takes two parameters - func2 (param1, param2), then we can not include it in the chain!


But after all, in practice, many functions take several parameters. So how do we combine them? Solution: Currying.


Currying is the transformation of a function that takes several parameters at a time into a function that takes only one parameter. The function will not be executed until all parameters have been passed.


In addition, currying can be used when referring to global variables, i.e., to do it “purely”.


Let's look again at our solution:


 // ,                // indexURLs     let indexURLs = { 'en': 'http://mysite.com/en', //English 'sp': 'http://mysite.com/sp', //Spanish 'jp': 'http://mysite.com/jp' //Japanese } //  const getUrl = (language) => allUrls[language]; //,   (   ),     //  // : const getUrl = (allUrls, language) => { return Maybe(allUrls[language]); } // : const getUrl = R.curry(function(allUrls, language) {//curry        return Maybe(allUrls[language]); }); const maybeGetUrl = getUrl(indexURLs) //   curried  . //   maybeGetUrl     ().     maybe(user).chain(maybeGetUrl).bla.bla 

Example 2. Work with functions throwing errors and exit immediately after an error occurs.


The section deals with: monad Either


The Maybe monad is very convenient if we have default values ​​for replacing Null errors. But what about functions that really need to throw errors? And how do you know which function threw an error if we have chained several error-throwing functions? That is, we need fast-failure.


For example: we have a chain func1.func2.func3 ... , and if func2 threw an error, then we need to skip func3 and the subsequent functions and correctly show the error from func2 for further processing.


Monad Either


Either monads are great for working with several functions, when any of them can throw an error and want to exit immediately after this, so that we can determine exactly where the error occurred.


Application: in the imperative code below, we calculate the tax and discount for the item and showTotalPrice .


Please note that the tax function will throw an error if the price value is non-numeric. For the same reason, throw the error and the function discount. But, besides, discount will throw an error if the price of the item is less than 10.


Therefore, showTotalPrice checks for errors.


 //  //  ,      const tax = (tax, price) => { if (!_.isNumber(price)) return new Error("Price must be numeric"); return price + (tax * price); }; //   ,      const discount = (dis, price) => { if (!_.isNumber(price)) return (new Error("Price must be numeric")); if (price < 10) return new Error("discount cant be applied for items priced below 10"); return price - (price * dis); }; const isError = (e) => e && e.name == 'Error'; const getItemPrice = (item) => item.price; //       .   . const showTotalPrice = (item, taxPerc, disount) => { let price = getItemPrice(item); let result = tax(taxPerc, price); if (isError(result)) { return console.log('Error: ' + result.message); } result = discount(discount, result); if (isError(result)) { return console.log('Error: ' + result.message); } //  console.log('Total Price: ' + result); } let tShirt = { name: 't-shirt', price: 11 }; let pant = { name: 't-shirt', price: '10 dollars' }; let chips = { name: 't-shirt', price: 5 }; //less than 10 dollars error showTotalPrice(tShirt) // : 9,075 showTotalPrice(pant) // :     showTotalPrice(chips) // :       10 

Let's see how you can improve showTotalPrice using the Either monad and rewrite everything in FP-style.


The Either Monad provides two constructors: Either.Left and Either.Right. They can be considered subclasses of Either. Left and Right are monads! The idea is to store errors / exceptions in Left, and useful values ​​in Right. That is, depending on the value, we create an instance of Either.Left or Either.Right. By doing this, we can apply map, chain, etc. to these values.


Although Left and Right provide map, chain, etc., the Left constructor only stores errors, and the Right constructor implements all functions, because it stores the actual result.


Now let's see how we can convert our imperative code into a functional one.


Stage 1. Wrap the return values ​​in Left and Right.


Note: “wrap” means “create an instance of some class”. These functions call new inside, so we don’t have to do it.


 var Either = require('ramda-fantasy').Either; var Left = Either.Left; var Right = Either.Right; const tax = R.curry((tax, price) => { if (!_.isNumber(price)) return Left(new Error("Price must be numeric")); //<--   Either.Left return Right(price + (tax * price)); //<--   Either.Right }); const discount = R.curry((dis, price) => { if (!_.isNumber(price)) return Left(new Error("Price must be numeric")); //<--Wrap Error in Either.Left if (price < 10) return Left(new Error("discount cant be applied for items priced below 10")); //<--    Either.Left return Right(price - (price * dis)); //<--   Either.Right }); 

Stage 2. We wrap the original value in Right , because it is valid and we can combine it (compose).


const getItemPrice = (item) => Right(item.price);


Step 3. Create two functions: one for error handling, the second for processing the result. Wrap them in Either.either (from ramda-fantasy.js api ).


Either.either takes three parameters: a result handler, an error handler, and the Either monad. Either is curried, so we can pass the handlers now, and Either (the third parameter) - later.


As soon as Either.either receives all three parameters, it passes Either to either the result handler or the error handler, depending on whether Either is Right or Left.


 const displayTotal = (total) => { console.log('Total Price: ' + total) }; const logError = (error) => { console.log('Error: ' + error.message); }; const eitherLogOrShow = Either.either(logError, displayTotal); 

Step 4. Use the chain method to combine functions throwing errors. Let's pass their results to Either.either (eitherLogOrShow), which takes care of relaying them to a result or error handler.


 const showTotalPrice = (item) => eitherLogOrShow(getItemPrice(item).chain(apply25PercDisc).chain(addCaliTax)); 

Putting it all together:


 const tax = R.curry((tax, price) => { if (!_.isNumber(price)) return Left(new Error("Price must be numeric")); return Right(price + (tax * price)); }); const discount = R.curry((dis, price) => { if (!_.isNumber(price)) return Left(new Error("Price must be numeric")); if (price < 10) return Left(new Error("discount cant be applied for items priced below 10")); return Right(price - (price * dis)); }); const addCaliTax = (tax(0.1));// 10 % const apply25PercDisc = (discount(0.25));//  25 % const getItemPrice = (item) => Right(item.price); const displayTotal = (total) => { console.log('Total Price: ' + total) }; const logError = (error) => { console.log('Error: ' + error.message); }; const eitherLogOrShow = Either.either(logError, displayTotal); //api const showTotalPrice = (item) => eitherLogOrShow(getItemPrice(item).chain(apply25PercDisc).chain(addCaliTax)); let tShirt = { name: 't-shirt', price: 11 }; let pant = { name: 't-shirt', price: '10 dollars' }; //error let chips = { name: 't-shirt', price: 5 }; //less than 10 dollars error showTotalPrice(tShirt) // : 9,075 showTotalPrice(pant) // :     showTotalPrice(chips) // :       10 

Example 3. Assigning Value to Potential Null Objects


*** FP-concept used: Applicative




Application: let's say you need to give the user a discount if he logged in and if the promotion is valid (i.e. there is a discount).


image


Let's use the applyDiscount method. It can throw null errors if the user (on the left) or discount (on the right) is null.


 //    user,    ,  . //Null-    ,      null. const applyDiscount = (user, discount) => { let userClone = clone(user);//   -    userClone.discount = discount.code; return userClone; } 

Let's see how you can solve this with the help of applicability.


Applicativeness


Any class that has the ap method and implements the Applicative specification is called applicative. Such classes can be used in functions that work with null-values ​​both on the left side (user) of the equation, and on the right side (discount).


The monads Maybe (like all monads) also implement the ap specification, which means they are also applicative, and not just monads. Therefore, at the functional level, we can use Maybe monads to work with null. Let's see how to make applyDiscount work with the Maybe monad, which is used as applicative.


Stage 1. Wrap the potential null values ​​in Maybe monads.


 const maybeUser = Maybe(user); const maybeDiscount = Maybe(discount); 

Step 2. Rewrite the function and curry it to pass one parameter at a time.


 //     ,  //       var applyDiscount = curry(function(user, discount) { user.discount = discount.code; return user; }); 

Step 3. Pass through map the first argument (maybeUser) to applyDiscount.


 //   map   (maybeUser)  applyDiscount const maybeApplyDiscountFunc = maybeUser.map(applyDiscount); // ,   applyDiscount   map    ,    (maybeApplyDiscountFunc)     Maybe  applyDiscount,       maybeUser( ). 

In other words, we now have a function wrapped in a monad!


Stage 4. We work with maybeApplyDiscountFunc.


At this stage, maybeApplyDiscountFunc may be:


1) a function wrapped in Maybe, if the user exists;
2) Nothing (Maybe subclass) - if the user does not exist.


If the user does not exist, then Nothing is returned, and all subsequent actions with it are completely ignored. So if we pass the second argument, nothing will happen. There will also be no null errors.


If the user exists, then we can try to pass the second argument to maybeApplyDiscountFunc via map to execute the function:


maybeDiscount.map(maybeApplyDiscountFunc)! // !


Oh-oh: map does not know how to perform the function (maybeApplyDiscountFunc) when it is inside Maybe!


Therefore, we need a different interface for working in this scenario. And this interface is ap!


Step 5. Refresh the information about the function ap. The ap method takes another Maybe monad and passes / applies the currently stored function to it.


 class Maybe { constructor(val) { this.val = val; } ... ... //ap   maybe        . //this.val     Nothing (      ) ap(differentMayBe) { return differentMayBe.map(this.val); } } 

We can simply apply (ap) maybeApplyDiscountFunc to maybeDiscount instead of using a map, as shown below. And it will work great!


 maybeApplyDiscountFunc.ap(maybeDiscount) // applyDiscount   this.val   maybeApplyDiscountFunc: maybeDiscount.map(applyDiscount) //,  maybeDiscount  ,   .  maybeDiscount  Null,    . 

For information: obviously, the specification of Fantasy Land made a change. In the old version it was necessary to write: Just (f) .ap (Just (x)), where f is a function, x is a value. In the new version you need to write Just (x) .ap (Just (f)). But the implementation for the most part has not changed. Thank you keithalexander .


Summarize. If you have a function that works with several parameters, each of which can be null, then curry it first and then place it inside Maybe. Also put all the parameters in Maybe, and use ap to execute the function.


CurryN function


We are already familiar with currying. This is a simple conversion of a function so that it takes not several arguments at once, but one at a time.


 // : const add = (a, b) =>a+b; const curriedAdd = R.curry(add); const add10 = curriedAdd(10);//  .  ,   (b) . //     . add10(2) // -> 12 //  add  10  2. 

But what if instead of adding just two numbers, the add function could summarize all the numbers passed to it as arguments?


const add = (...args) => R.sum(args); //


We can still curry it by limiting the number of arguments with curryN :


 // curryN const add = (...args) => R.sum(args); // CurryN: const add = (...args) => R.sum(args); const add3Numbers = R.curryN(3, add); const add5Numbers = R.curryN(5, add); const add10Numbers = R.curryN(10, add); add3Numbers(1,2,3) // 6 add3Numbers(1) //  ,    . add3Numbers(1, 2) //  ,    . 

Using curryN to wait for the number of function calls


Suppose we need a function that writes to the log only when we called it three times (one and two calls are ignored). For example:


 //  let counter = 0; const logAfter3Calls = () => { if(++counter == 3) console.log('called me 3 times'); } logAfter3Calls() //    logAfter3Calls() //    logAfter3Calls() // 'called me 3 times' 

We can emulate this behavior with curryN.


 //  const log = () => { console.log('called me 3 times'); } const logAfter3Calls = R.curryN(3, log); // logAfter3Calls('')('')('')//'called me 3 times' //:   '',   CurryN   . 

Note: we will use this technique for applicative validation.


Example 4. Collecting and displaying errors


The section deals with: validation (known as the Validation functor, the applicability of Validation, the Validation monad).


Validation (Validation Applicative) , ap (apply).


Either , . Either, chain, Validation ap. , chain , ap, Validation, .


, .


: , , (isUsernameValid, isPwdLengthCorrect ieEmailValid). , .


image


Validation. , Validation.


folktalejs data.validation, ramda-fantasy . Either : Success Failure . , Either.


1. Success Failure (. . ).


 const Validation = require('data.validation') // folktalejs const Success = Validation.Success const Failure = Validation.Failure const R = require('ramda'); //: function isUsernameValid(a) { return /^(0|[1-9][0-9]*)$/.test(a) ? ["Username can't be a number"] : a } //: function isUsernameValid(a) { return /^(0|[1-9][0-9]*)$/.test(a) ? Failure(["Username can't be a number"]) : Success(a) } 

, .


2. (dummy function) .


const returnSuccess = () => 'success';// success


3. curryN ap


ap : ( ), .


, ap. , monad1 . monad1.ap(monad2), . . resultingMonad , , ap monad3.


 let finalResult = monad1.ap(monad2).ap(monad3) //   let resultingMonad = monad1.ap(monad2) let finalResult = resultingMonad.ap(monad3) // ,  monad1  ,   monad1.ap(monad2)    (resultingMonad)   

, ap, , .


, ap.


, - .


  Success(returnSuccess) .ap(isUsernameValid(username)) // .ap(isPwdLengthCorrect(pwd))//  .ap(ieEmailValid(email))//  

, Success(returnSuccess).ap(isUsernameValid(username)) . ap .


curryN, , N .


 //3 —    ap  . let success = R.curryN(3, returnSuccess); 

success .


 function validateForm(username, pwd, email) { //3 —    ap  . let success = R.curryN(3, returnSuccess); return Success(success)//  ;    ap .ap(isUsernameValid(username)) .ap(isPwdLengthCorrect(pwd)) .ap(ieEmailValid(email)) } 

:


 const Validation = require('data.validation') // folktalejs const Success = Validation.Success const Failure = Validation.Failure const R = require('ramda'); function isUsernameValid(a) { return /^(0|[1-9][0-9]*)$/.test(a) ? Failure(["Username can't be a number"]) : Success(a) } function isPwdLengthCorrect(a) { return a.length == 10 ? Success(a) : Failure(["Password must be 10 characters"]) } function ieEmailValid(a) { var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; return re.test(a) ? Success(a) : Failure(["Email is not valid"]) } const returnSuccess = () => 'success';//  success function validateForm(username, pwd, email) { let success = R.curryN(3, returnSuccess);// 3 —    ap  . return Success(success) .ap(isUsernameValid(username)) .ap(isPwdLengthCorrect(pwd)) .ap(ieEmailValid(email)) } validateForm('raja', 'pwd1234567890', 'r@r.com').value; //Output: success validateForm('raja', 'pwd', 'r@r.com').value; //Output: ['Password must be 10 characters' ] validateForm('raja', 'pwd', 'notAnEmail').value; //Output: ['Password must be 10 characters', 'Email is not valid'] validateForm('123', 'pwd', 'notAnEmail').value; //['Username can\'t be a number', 'Password must be 10 characters', 'Email is not valid'] 

')

Source: https://habr.com/ru/post/327522/


All Articles