Editor’s Note: This is the first installment of the “Cut the crap, Get straight to The Facts” column, which introduces you to the functional programming paradigm and gives you a quick introduction to the use of functional programming.
First, according to? Why learn and use functional programming?
First, before we introduce what functional programming is, we need to briefly understand why we need to learn and use “functional programming.”
The reasons are summarized as follows:
- Functional programming has received increasing attention with the popularity of React;
- Vue3 is also embracing functional programming;
- Functional programming can be abandoned
this
; - The packaging process can be better utilized
tree shaking
Filter useless code; - Convenient for testing and parallel processing;
- There are many third-party libraries that can help us with functional development, such as Lodash, underscore, and Ramda.
Second, WHAT? What is functional programming?
In fact, functional programming is a very old concept, predating even the first computers. The theoretical basis for functional programming is the Lambda calculus developed by Alonzo Church in the 1930s. It is itself a mathematical abstraction rather than a programming language. It was later introduced into programming languages.
In short, functional Programming (FP) is a Programming paradigm. Other paradigms we hear about are “procedural programming” and “object-oriented programming” :
- (1) The way of thinking of object-oriented programming: abstract the things in the real world into classes and objects in the program world, and demonstrate the connection between things through encapsulation, inheritance and polymorphism.
- (2) Functional programming way of thinking: the real world things and things betweencontactAbstracting functions from the program world (abstracting operations)
- The essence of the program: according to the input through some operation to obtain the corresponding output. The process of program development involves many input and output functions.
- 2.A function in functional programming is not a function (method) in a program, but a function in mathematics, a mappingFor example, y=sin(x), x and y.
- ③ A very important concept in functional programming is “pure function”. The biggest characteristic of a pure function (or its definition) is that the same input always produces the same output. (We’ll talk about pure functions later.)
- (4) In short, functions in functional programming are used to describe mappings between data.
- (3) Process-oriented programming because we are not involved, we do not do research here.
The following code can help you get a sense of what functional programming is:
// Non-functional programming
let num1 = 2;
let num2 = 3;
let sum = num1 + num2;
console.log(sum);
// Functional programming
function add (n1, n2) {
return n1 + n2;
}
let sum = add(2.3);
console.log(sum);
Copy the code
Recommended reading
If you want to know more about functional programming, I recommend reading Ruan Yifeng’s “Functional Programming”.
I will not repeat it here.
Functions are first-class citizens
A Function is a first-class Function. It is mainly reflected in:
- Functions can be stored in variables;
- Functions can be arguments;
- A function can be a return value.
In JavaScript, a Function is just an ordinary object (which can be created with new Function()). Based on this, we can store functions in variables/arrays, use functions as arguments and return values of another Function, and even construct a new Function while the Function is running by new Function(‘alert(1)’).
Code demo:
// Assign a function to a variable
let fn = function () {
console.log('Hello First-class Function');
}
fn();
// An example
const BlogController = {
index (post) { return View.index(post) },
show (post) { return View.show(post) },
create (attrs) { return Db.create(attrs) },
update (post, attrs) { return Db.update(post, attrs) },
destroy (post) { return Db.destroy(post) },
};
// In fact, functions like index and show are equivalent to the view. index and view. show functions themselves, so the above objects can be optimized as follows:
cosnt blogController = {
index: View.index,
show: View.show,
create: Db.create,
update: Db.update,
destroy: Db.destroy
}
// function as an argument to another function
let fn = function () {
console.log('Hello First-class Function');
};
let executeFn = function (fn) {
fn();
};
executeFn();
// function as return value
let returnFn = function () {
let fn = function () {
console.log('Hello First-class Function');
};
return fn;
};
returnFn();
Copy the code
Higher order functions
Higher-order function, namely:
- (1) You can pass a function as an argument to another function;
- (2) A function can be returned as a result of another function.
(a) Functions can be used as arguments
// Write array forEach method
function forEach(array, func) {
for (let i = 0; i < array.length; i++) { func(array[i]); }};/ / test
let arr = [1.3.4.7.8];
let fn = function (item) {
console.log(item);
};
forEach(arr, fn); // Output: 1 3 4 7 8
Copy the code
// The filter method for the handwritten array
function filter(array, func) {
let result = [];
for (let i = 0; i < array.length; i++) {
if(func(array[i])) { result.push(array[i]); }}return result;
}
/ / test
let arr = [1.3.4.7.8];
let fn = function (item) {
return item % 2= = =0;
};
let result = filter(arr, fn);
console.log(result); // Output result: [4, 8]
Copy the code
(2) functions can be used as return values
function makeFn () {
let message = "Hello Function";
return function () {
console.log(message);
};
};
const fn = makeFn(); // function () {console.log(message); };
fn(); // "Hello Function"
Copy the code
// once function -- The passed function is executed only once, no matter how many times the function is executed
// The once function is a classic use of a function as a return value (higher-order function)
function once(func) {
let done = false;
return function () {
if(! done) { done =true;
return func.apply(this.arguments); // The argument passed by apply is an array or array-like object}}; }// Use the once function
let pay = once(function (money) {
console.log(`you've payed ${money} RMB`);
});
/ / test
pay(5); // Output: You've payed 5 RMB
pay(5); / / no output
pay(5); / / no output
pay(5); / / no output
pay(5); / / no output
// Execution result: No matter how many times we execute the pay function, the function passed in once is executed only once
Copy the code
(3) the meaning of higher-order functions
The significance of using higher-order functions is:
- The abstraction of higher-order functions helps us mask the details, and we only need to care about our goals;
- Higher-order functions are used to abstract general problems, are generally universal, and can be used repeatedly.
- The use of higher-order functions, such as the forEach and filter functions, makes our functions more flexible.
(4) commonly used higher-order functions
ForEach, Map, filter, every, some, find, findIndex, reduce, sort…
Let’s simulate some higher-order functions — map, every and some:
// The handwritten array map method
const map = (array, func) = > {
let result = [];
for (const value of array) {
result.push(func(value));
}
return result;
};
/ / test
let arr = [1.2.3.4];
arr = map(arr, (value) = > value ** 2); / / exponentiation
console.log(arr); // [1, 4, 9, 16]
Copy the code
// Write the array every method
const every = (array, func) = > {
let result = true;
for (const value of array) {
result = func(value);
if(! result) {break; }}return result;
};
/ / test
const arr1 = [11.12.14];
const arr2 = [9.12.14];
const res1 = every(arr1, (value) = > value > 10);
const res2 = every(arr2, (value) = > value > 10);
console.log(res1, res2); // true false
Copy the code
// Write the array some method
const some = (array, func) = > {
let result = false;
for (const value of array) {
result = func(value);
if (result) {
break; }}return result;
};
/ / test
const arr1 = [1.3.4.9];
const arr2 = [1.3.5.9];
const res1 = some(arr1, (value) = > value % 2= = =0);
const res2 = some(arr2, (value) = > value % 2= = =0);
console.log(res1, res2); // true false
Copy the code
Six, closures
(1) The concept of closure
Closures: Closures are formed when a function is bundled with references to its surrounding state (lexical context). Visually, you can call an inner function of a function from another scope and access members of that function’s scope. Take the makeFn and once functions we implemented above.
(2) The nature of closures
The essence of a closure is that a function is placed on an execution stack at execution time. When the function completes, it is usually removed from the execution stack, but the inner function can still access the members of the outer function because the function cannot be removed because the scoped members on the heap are referred to externally.
(3) the function of closures
Closures extend the duration of the internal variables of an external function.
The obvious benefits are:
- Protect variables inside functions from external scopes;
- The internal variables of the function are saved so that they are not removed (freed up memory) after the function completes execution.
The downside of closures is equally obvious: if you use too many closures, you can end up with a lot of memory being held up for too long, and the end result is that your code can become very slow to run.
So, closures are a good idea, but don’t overdo it
(4) Closure case demonstration
Here are two examples of closures in action:
- Case 1 — Finding the power of a number:
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
<title>closure</title>
</head>
<body>
<script>
// How to power a number
function makePower (power) {
return function (number) {
return Math.pow(number, power);
};
};
// When we need to power a fixed power multiple times, we can use the closure mechanism to generate a corresponding function first
const power2 = makePower(2); // Create a special method for finding squares
const power3 = makePower(3); // Create a special method for cubing
// Execute (this keeps the code concise and improves our writing efficiency)
console.log(power2(4)); // 16 (square 4)
console.log(power2(5)); // 25 (square 5)
console.log(power3(2)); // 8 (2 cubed)
console.log(power3(3)); // 27 (find 3 cubed)
</script>
</body>
</html>
Copy the code
- Example 2 — Write a way to get your salary:
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
<title>closure</title>
</head>
<body>
<script>
function getSalary (base) {
return function (permance) {
return base + permance; // Base = base; permanence = performance pay}}const salaryLevel1 = getSalary(12000); // Salary level 1: Basic salary 12000 yuan
const salaryLevel2 = getSalary(15000); // Salary level 2: Basic salary 15000
console.log(salaryLevel1(2000)); // The basic salary is 12000, and the performance salary is 2000 hours
console.log(salaryLevel1(5000)); // The basic salary is 12000, and the performance salary is 5000 hours
console.log(salaryLevel2(3000)); // Basic salary 15000, performance salary 3000 hours
console.log(salaryLevel2(5000)); // Base salary of 15000, performance salary of 5000 hours
</script>
</body>
</html>
Copy the code
7. Pure functions
Note: ① Starting in this section, we’ll use Lodash, the high-performance JavaScript utility library. You’ll need to install the Lodash module locally. For more information about Lodash, please refer to www.lodashjs.com/
(1) The concept of pure functions
In functional programming, functions refer to pure functions. A pure function is one in which the same input always yields the same output without any observable side effects.
The definition of a pure function is similar to a function in mathematics that describes the relationship between inputs and outputs, such as y=f(x).
If you’ve used some third-party JS libraries, they also provide a lot of pure function functionality. Lodash, for example, is a library of pure functions. It provides operations on arrays, numbers, objects, strings, and functions, many of which are pure functions.
There are many pure function methods, such as slice, as well as everyday array methods. By contrast, splice, the other method in the array, is not a pure function (or impure function). Let’s compare pure and impure functions:
Slice is a pure function that returns a specified portion of an array without changing the original array.
// The impalent function splice, which can delete, add, or replace an array, returns the modified portion (in the form of an array) that changes the original array.
let array = [1.2.3.4.5];
// When using slice, the same input corresponds to the same output
console.log(array.slice(0.3)); / / [1, 2, 3]
console.log(array.slice(0.3)); / / [1, 2, 3]
console.log(array.slice(0.3)); / / [1, 2, 3]
// When using the impure function splice, the same input can produce different outputs
console.log(array.splice(0.3)); / / [1, 2, 3]
console.log(array.splice(0.3)); / / [4, 5]
console.log(array.splice(0.3)); / / []
Copy the code
Functional programming usually does not preserve the results of the computation, so variables are immutable, or stateless (that is, functional programming does not affect the values of variables that are input and output to a function; their values do not change as they are used by the function, so there is no process of change). Therefore, we can pass the results of one function to another (that is, we can make chained calls, which will be covered later in Lodash).
(2) Advantages of pure functions
The advantages of pure functions are:
- Can cache;
- Because pure functions always have the same output for the same input, the results of pure functions can be cached. For example, memory functions in Lodash
memoize
Methods:
// Memoize method in LoDash const _ = require('lodash'); // Find the area of the circle function getArea (r) { console.log(r); return Math.PI * r * r; }; let getAreaWithMemory = _.memoize(getArea); console.log(getAreaWithMemory(4)); / / 4 50.26548245743669 console.log(getAreaWithMemory(4)); / / 50.26548245743669 console.log(getAreaWithMemory(4)); / / 50.26548245743669 // The memoize method in LoDash caches the result of getArea after the first execution, so the subsequent execution of getArea with the same input does not repeat, but directly returns the cached value of gertArea. Copy the code
- Implementation of memoize method in LoDash
// Simulate the implementation of the memorize method function memorize(func) { const cache = {}; return function () { const key = JSON.stringify(arguments); // The function argument (the input at the time of the function execution) is converted to a string, which is then used as an attribute of the cache object above. Arguments here is the passing argument to the anonymous function (the current function), and arguments is a pseudo-array. cache[key] = cache[key] || func.apply(func, arguments); // If there is a corresponding key/value pair in the cache, the value from the cache is fetched; If arguments do not exist in the cache, pass the arguments to the function, and the result of the function is cached while the function executes return cache[key]; }; } / / test function getArea(r) { console.log(r); return Math.PI * r * r; } const getAreaWithMemory = memorize(getArea); console.log(getAreaWithMemory(4)); / / 4 50.26548245743669 console.log(getAreaWithMemory(4)); / / 50.26548245743669 console.log(getAreaWithMemory(4)); / / 50.26548245743669 Copy the code
- Because pure functions always have the same output for the same input, the results of pure functions can be cached. For example, memory functions in Lodash
- Testable Because pure functions always have inputs and outputs, and unit tests assert the results of that function, all of our pure functions are testable. Therefore, pure functions make testing easier.
- Parallel processing (just for understanding) In a multi-threaded environment, it is possible to run shared memory data in parallel (for example, if you are operating on the same data at the same time, you are not sure what the final data will look like because of the order in which the operations are completed). A pure function is a closed space. Pure functions do not need to access shared memory data, so they can be run arbitrarily in parallel. JavaScript used to be completely single-threaded, but ES6 has added a Web Worker that can start new threads to improve performance in turn. So now JavaScript can sometimes be multithreaded. But most of the time JavaScript is still single-threaded, so parallel processing is just a matter of understanding.
(3) Side effects of pure functions
- What are side effects?
When we define pure functions, we say, “For the same input you always get the same output, without any observable side effects.” The observable side effect here is that a function that depends on external state cannot guarantee the same output from the same input. Side effects can make a function impure. Such as:
// Impure function, same input will get different output if dependent external state changes.
let min = 18;
function checkAge (age) {
return age >= min;
};
// A pure function does not depend on external states. The same input always gets the same output.
function checkAge (age) {
let min = 18;
return age >= min;
};
/ / or
function checkAge (min, age) {
return age >= min;
};
Copy the code
- Source of side effects.
All external interactions can cause side effects, such as:
- The configuration file
- The database
- Get user input
- …
- The impact of side effects
- Side effects make the method less universal, not suitable for expansion, low reusability;
- At the same time, side effects can bring security risks and uncertainty to the program.
- Side effects can’t be completely banned, we have to try to keep them under control as much as possible.
8. Haskell Brooks Curry
(1) What is Corrification?
Currification of a function means that when a function has multiple arguments, it is called by passing some of the arguments (which will never change), then a new function is returned to accept the remaining arguments, and then the new function is executed to return the result. That is:
- When a function has more than one argument, pass some of the call function first.
- When a function is called, a new function is returned to accept the remaining arguments;
- Passing the remaining arguments to execute the returned new function returns the final result.
(2) The Case of Corrification
For example, the checkAge function above can be currified:
// a plain pure function
function checkAge (min, age) {
return age >= min;
};
// Plain pure function execution
console.log(checkAge(18.20));
console.log(checkAge(18.24));
console.log(checkAge(20.18));
console.log(checkAge(20.24));
// This is a pure function
function checkAgeCurry (min) {
return function (age) {
return age >= min;
};
};
// Use the arrow function of ES6 to simplify:
let checkAgeCurry = min= > (age= > age >= min);
// The execution of a currie pure function
let checkAgeCurry18 = checkAgeCurry(18);
let checkAgeCurry20 = checkAgeCurry(20);
console.log(checkAgeCurry18(20));
console.log(checkAgeCurry18(24));
console.log(checkAgeCurry20(18));
console.log(checkAgeCurry20(24));
Copy the code
The match and filter methods of an array are currified:
// Here we need to introduce Lodash and use the curry method it provides
const _ = require('lodash');
// Convert the match method to a pure function and currize it
const match = _.curry(function (reg, str) {
return str.match(reg);
});
const haveSpace = match(/\s+/g); // A method that matches all whitespace characters in a string
const haveNumber = match(/\d+/g); // A method to match all numeric characters in a string
// Convert the filter method to a pure function and currize it
const filter = _.curry(function (func, array) {
return array.filter(func);
});
const findSpace = filter(haveSpace);
/ / test
console.log(filter(haveSpace, ['John Conner'.'John_Donne'])); // [ 'John Conner' ]
console.log(findSpace(['John Conner'.'John_Donne'])); // [ 'John Conner' ]
Copy the code
(3) Simulation of The Cremation principle
Let’s simulate implementing the Curry method in LoDash:
// Simulate the implementation of curry's method
function curry(func) {
return function curriedFunc(. args) {
// Determine the number of real parameters and parameters
if (args.length < func.length) {
// When the number of arguments passed is less than the number of parameters before the function was currified, a new function is returned waiting to accept the remaining arguments
return function () {
returncurriedFunc(... args.concat(Array.from(arguments))); // Concatenate the two passed arguments into a new array, which is then deconstructed and passed to the function for execution
};
}
returnfunc(... args); }; }/ / test
function getSum(a, b, c) {
return a + b + c;
};
const curried = curry(getSum);
console.log(curried(1.2.3)); / / 6
console.log(curried(1) (2.3)); / / 6
Copy the code
(4) Summary of Currization
- First, Corrification allows us to pass fewer arguments to a function and get a new function that has memorized some of the fixed arguments;
- Second, it is a “cache” of function parameters.
- Currization can make functions more flexible and smaller in granularity;
- Kerrization can transform multivariate functions into unary functions, and can combine functions to produce powerful functions.
Combination function/function combination
(1) The concept of function combination
- Why use function combinations
Pure functions and Currization are easy to write in onion code like H (g(f(x)). For example, if you use loDash to get the last element of the array and convert it to uppercase, you might write something like this:
const _ = require('lodash');
const array = ['aaa'.'bbb'.'ccc'.'ddd'];
const result = _.toUpper(_first(_.reverse(array))); // This line of code becomes onion code, one layer on top of the other
console.log(result);
Copy the code
On the one hand, such code is very error-prone to write, on the other hand, maintenance is also very troublesome, very easy to head. So we introduced the concept of combination of functions to solve this situation.
So, the first thing we want to make clear is that the purpose, or purpose, of function composition is to allow us to recombine fine-grained functions to produce a new function that improves the readability and maintainability of our code.
- The pipe
Before we formally introduce the concept of function composition, we need to introduce another concept — pipes.
“Pipeline” is not a serious term, but a concept we are using for the moment. When we use a function to process data, we can think of it as a pipeline. When a function is complex, we can break it up into smaller functions, and we don’t need to worry about the operations in between (including temporary variables generated during the operations and so on). It’s like taking a very complicated pipe and breaking it up into a lot of short pipes, although it’s hard to make the whole complex pipe, it’s easy to make a lot of simple pipes, and then we put together a lot of short pipes to make a whole pipe that is very complicated at the beginning. This process of stitching together is the concept of “composition of functions” that we will talk about next.
- Function composition
Compose: If a function needs to be processed by multiple functions to get the final value, you can combine the intermediate functions into a single function. To explain this, functions are like pipelines of data, and composition of functions is all about connecting these pipes and passing data through multiple pipes to form the final result.
Note that function combinations are executed from right to left by default. This is because the internal execution of function combinations still ends up as onion code (we just can’t see it). By default, functions are executed with the front in the outer layer and the back in the inner layer, while code is executed from the inside out.
// The function combination method can only combine two functions to demonstrate the principle
function compose(f, g) {
return function (value) {
return f(g(value));
};
};
/ / test
function reverse(array) {
return array.reverse();
};
function first(array) {
return array[0];
};
const last = compose(first, reverse); Array [array.length-1] = array[array.length-1] = array[array.length-1] = array[array.length-1]
console.log(last([1.2.3.4])); / / 4
Copy the code
(2) Introduction of combinatorial functions in Lodash
There are two combinatorial functions in Lodash:
flow
, the combined functions are executed from left to right;flowRight
The combined function is executed from right to left, and this function tends to be used more often.
// The flowRight method is used in the same way as the flowRight method.
const _ = require('lodash');
const reverse = arr= > arr.reverse();
const first = arr= > arr[0];
const toUpper = str= > str.toUpperCase();
const func = _.flowRight(toUpper, first, reverse); // Take the last item of the array and uppercase it
console.log(func(['one'.'two'.'three'])); // 'THREE'
Copy the code
(3) the principle simulation of function combination
Let’s simulate the compose method in Lodash:
// Simulate the implementation of the compose method
function compose(. args) {
return function (value) {
return args.reverse().reduce(function (accumulator, currentFunc) {
return currentFunc(accumulator); // accumulator: is the return value of the callback from the previous call. CurrentFunc: The element being processed in the array, that is, the function currently being executed.
}, value);
};
}
// We can use ES6's arrow function to further simplify:
const composeES6 =
(. args) = >
(value) = >
args.reverse().reduce((acc, fn) = > fn(acc), value);
/ / test
const reverse = (arr) = > arr.reverse();
const first = (arr) = > arr[0];
const toUpper = (s) = > s.toUpperCase();
const f = composeES6(toUpper, first, reverse);
console.log(f(["one"."two"."three"])); // 'THREE'
Copy the code
(4) Associative law of function combination
The combination of functions should satisfy the associativity law, which means that we can combine any neighboring function and then combine it with other functions or other combined functions to get the same result.
// associative property:
let f = compose(f, g, h);
let assciativity1 = compose(compose(f, g), h);
let associativity2 = compose(f, compose(g, h));
console.log(assciativity1 == f); // true (where equality only means that two functions are equivalent, does not mean that the memory address of the two functions is equal)
console.log(assciativity2 == f); // true (where equality only means that two functions are equivalent, does not mean that the memory address of the two functions is equal)
Copy the code
// Associative law of function combinations in lodash
const _ = require('lodash');
const arr = ['one'.'two'.'three'];
const f1 = _.flowRight(_.toUpper, _.first, _.reverse); // toUpper, first and reverse are methods provided in Lodash, which have the same function as the same function we wrote earlier.
const f2 = _.flowRight(_.toUpper, _.flowRight(_.first, _.reverse));
const f3 = _.flowRight(_.flowRight(_.toUpper, _.first), _.reverse);
console.log(f1); // 'THREE'
console.log(f2); // 'THREE'
console.log(f3); // 'THREE'
Copy the code
(5) Debugging of function combination
Another problem with function composition is how to debug a program if it has a problem and find out what the problem is. In fact, the operation is very simple, we just need to write a trace function and put it in the appropriate position of the function combination.
Here we use code to demonstrate:
// Need: convert uppercase "NEVER SAY DIE" to lowercase "NEVER say-die"
const _ = require('lodash); Const split = _. Curry ((separator, string) => _. Split (string, separator)); Const join = _. Curry ((connector, array) => _. Join (array, connector)); // Const map = _.curry((func, array) => _.map(array, fn)); Curry ((tag, value) => {console.log(tag, value); // ⭐️ ⭐️ const trace = _. Curry ((tag, value) => {console.log(tag, value); // tag is the identifier used to mark the location of the function. Value is the return value of the last function. }); // When combining functions, add trace functions to the combination, and the corresponding identifier, so that you can trace the location of the function problem. const fn = _.flowRight(join('-'), trace('Before join'), map(_.toLower), trace('The map function'), split(' ')); // You can also change the identifier to "after XXX" console.log(fn("NEVER SAY DIE")); // Add the trace function, we can see the result of each step of the function combination at a glance. If a small function in the middle has a problem, it can quickly locate the problemCopy the code
FP module in Lodash
There is also an FP module in LoDash that provides a practical approach to functional programming that is friendly. Functional programming-friendly means that lodash/ FP offers an “auto-curried iteratee-first data-last” approach.
- Automatic Kerrytization: fp module functions are already through kerrytization processing, can be directly used without any further kerrytization processing.
- Priority iteration: If a function in the FP module has a function (iterator) as an argument, the function argument is required to be first.
- Data last: In the parameters of a function in the FP module, the data to be processed is required to be placed at the end of the parameters.
Fp module brings us the biggest convenience is that all methods do not need to consider to do the Coriolization processing, and it makes us focus on how to deal with the method and function called, which allows us to concentrate on processing data.
Demo code:
// Requirement: convert each item in the array to uppercase
const arr = ['a'.'b'.'c'];
// when using the lodash method:
const _ = require('lodash');
const result = _.map(arr, _.toUpper); // Data first, iteration last
console.log(result);
// when using lodash/fp methods:
const fp = require('lodash/fp');
const resultFp = fp.map(fp.toUpper, arr); // Iteration first, data later
/ / or
const resultFpCurry = fp.map(fp.toUpper)(arr); // The methods in the FP module are all currified
console.log(resultFp);
console.log(resultCurry);
Copy the code
Another example of the requirements in the code demonstrated in the previous section would be very simple if handled using the methods in the FP module:
// "NEVER SAY DIE" --> "never-say-die"
const fp = require('lodash/fp');
const fn = fp.flowRight(fp.join(The '-'), fp.map(fp.toLower), fp.split(' '));
console.log(fn("NEVER SAY DIE"));
Copy the code
Small problems with the Map method in Lodash
Since there are differences between the map methods in LoDash and loDash/FP, this can cause problems when using LoDash’s map method with the built-in parseInt method, whereas fp’s map method does not.
Let’s use code to demonstrate this problem;
- When using the Map method in LoDash
// The map method in lodash is used with parseInt
const _ = require('lodash');
const result = _.map(['23'.'8'.'10'].parseInt);
console.log(result); [23, NaN, 2]
Copy the code
The results were quite different from what we expected. This is because the iterating function, the second argument to the Map method in LoDash, takes three arguments in order (see figure below, taken from the official documentation) :
- ① Each element in the array to be processed;
- ② The index of each element;
- (3) the original array
The parseInt method can only accept two parameters at most, which are (as shown below, extracted from MDN) :
- ① The value to be parsed;
- ② Specify the base number of the parsed value. The valid value ranges from 2 to 36. This parameter is optional.
This causes the Map method in LoDash to pass the index of each item in the first parameter array to the second parameter of parseInt, causing parseInt to parse each item in the array in terms of its index. For base questions, please refer to the second half of the description of the parseInt method in MDN:
- Parse ’23’ in decimal (0 parseInt will infer the corresponding base number based on the actual data condition, non-0 will be inferred as decimal), the result is the original number 23;
- Parse ‘8’ in base (since 1 does not fit the valid value range of the argument, i.e., there is no base number, so the parse result is not a number, i.e., NaN);
- Parsing ’10’ in binary yields 2.
- This problem does not occur when using the map method in Lodash/FP:
// the map method in lodash/fp is used with parseInt
const fp = require('lodash/fp');
const resultFp = fp.map(parseInt['23'.'8'.'10']);
console.log(resultFp); // Output result: [23, 8, 10]
Copy the code
This is because the map method in the Lodash/FP module takes an iterator function as its first argument and an array as its second argument. And the iterator to the FP module’s Map method passes only one parameter, the value of each item in the second parameter array.
Note this if you use Lodash’s map method.
Point Free
For more information on Point Free, read: Ruan Yifeng’s Pointfree Programming Style Guide. Address: www.ruanyifeng.com/blog/2017/0…
Point Free: we can define the process of data processing into synthesis arithmetic of has nothing to do with the data, the parameters representing data is not required, just put some simple operation steps synthesis together (like a mathematician USES a simple logic operation can solve the problem of complex formula is deduced, and don’t care about what is to compute the data of concrete).
Of course, before using this pattern, we need to define some basic operation functions that are not provided. Of course, the synthesis operation will definitely use the combination of functions we mentioned earlier.
Ruan Yifeng translated “Point Free” into “no value style”.
In fact, we already used the idea of Point Free in part 10, “FP Modules in Lodash,” in our demo code for using FP modules (click to see the code). Next, let’s use another case to demonstrate Point Free:
// Need: extract the first letter of a string, convert it to uppercase, then split it with a ". ", and finally concatenate the string.
// "world wild web" => "W. W. W"
const fp = require('lodash/fp');
const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.flowRight(fp.first, fp.toUpper)), fp.split(' ')); // Perform the compositing operation as required, regardless of data
console.log(firstLetterToUpper("world wild web)); // 'W. W. W'
Copy the code
Functor — Functor
If want to know more detailed functor related knowledge, you can refer to nguyen other teacher’s functional programming tutorial, address: www.ruanyifeng.com/blog/2017/0… .
(1) The concept of functors
Container: An object that contains values and their morphing relationships. A functor is a special container. By convention, a marker for a functor is that the container has a map method. The map method maps every value in a container to another container. (Map methods are interfaces that receive and return data from the outside.)
Here’s a simple example of how to use functors:
/ / Functor Functor
class Container1 {
constructor(value) { / / value: the value
this._value = value;
}
map(func) { // func: deformation relation
return new Container1(func(this._value)); }}/ / test
let r1 = new Container195).map(x= > x + 1).map(x= > x ** 2);
console.log(r1); // Container1 {_value: 36}
// Every time we execute the class, we need new to handle it. It is not convenient to write, so we can wrap the new step:
class Container2 {
static of(value) {
return new Container2(value);
}
constructor(value) {
this._value = value;
}
map(func) {
return Container2.of(func(this._value)); }};/ / test
// We can make the previous operation as simple as the following:
let r2 = Container2.of(5).map(x= > x + 1).map(x= > Math.pow(x, 2));
console.log(r2); // Container2 {_value: 36}
// Container1 and Container2 are functors
Copy the code
(2) Key points of functors
- The operations of functional programming do not operate directly on values, but are performed by functors.
- A functor is an object that implements the Map contract.
- We can think of a functor as a box that encapsulates a value.
- We need to deal with the values in the box, we need to give to the box
map
Method passes a function (pure function) that handles the value. - In the end
map
Method returns a functor containing the new value. - Functor side effects: Functor execution may fail when we pass in values like null, undefined, etc.
// Side effects of functors
class Container {
static of(value) {
return new Container(value);
}
constructor(value) {
this._value = value;
}
map(func) {
return Container.of(func(this._value)); }}/ / test
let result = Container.of(null).map(x= > x.toUpperCase());
console.log(result); TypeError: Cannot read property 'toUpperCase' of null TypeError: Cannot read property 'toUpperCase' of null Because there is no toUpperCase method on NULL.
Copy the code
(3) MayBe functor
Starting with this section, we introduce some of the more common and important functors.
As in the previous section, we may encounter many errors during programming and need to handle them accordingly. The MayBe functor is used to deal with external null cases (to keep null side effects within permissible limits).
For the code in the previous section, we further improved it to address the side effects of control as follows:
/ / MayBe functor
class MayBe {
static of(value) {
return new MayBe(value);
}
constructor(value) {
this._value = value;
}
map(func) {
return this.isNothing ? MayBe.of(null) : MayBe.of(func(this._value));
}
isNothing() {
return this._value === null || undefined;
// return this._value? false : true;}}/ / test
let r1 = MayBe.of('Hello World').map(x= > x.toUpperCase());
console.log(r1); // MayBe {_value: 'HELLO WORLD'}
let r2 = MayBe.of(null).map(x= > x.toUpperCase());
console.log(r2); // MayBe {_value: null}
let r3 = MayBe.of('Hello World')
.map(x= > x.toUpperCase())
.map(x= > null)
.map(x= > x.split(' '));
console.log(r3); // MayBe { _value: null }
Copy the code
In R3, the output is correct, but we can’t determine which step prints NULL. So we need to resort to the Either functor in the next section.
(4) Either functor
Either means Either of the two. In functional programming, Either functors are similar to if… else… Processing. Since exceptions make functions impure, Either functors can also be used for exception handling.
Common uses of Either functors:
- Provide a default value (if an rvalue has a value, use an rvalue, otherwise use an lvalue. In this way, Either functors express conditional operations.)
- Instead of
try... catch
, using an lvalue to indicate an error.
The Either functor can be written like this:
The Left and Right classes are Either functors
class Left {
static of(value) {
return new Left(value);
}
constructor(value) {
this._value = value;
}
map(func) {
return this; }}class Right {
static of(value) {
return new Right(value);
}
constructor(value) {
this._value = value;
}
map(func) {
return Right.of(func(this._value)); }}/ / test
let r1 = Right.of(12).map(x= > x + 2);
let r2 = Left.of(12).map(x= > x + 2);
console.log(r1); // Right {_value: 14}
console.log(r2); // Left {_value: 12}
function parseJSON(str) {
try {
return Right.of(JSON.parse(str));
} catch(e) {
return Left.of({error: e.message}); }}let r = parseJSON('{name: zs}');
console.log(r); // Left {_value: {error: 'Unexpected token n in JSON at position 1'}}
Copy the code
You can also write:
class Either {
static of(left, right) {
return new Either(left, right);
}
constructor(left, right) {
this._left = left;
this._right = right;
}
map(func) {
return this._right ?
Either.of(this._left, func(this._right)) :
Either.of(func(this._left), this._right); }}/ / test
let r1 = Either.of(5.6).map((x) = > x + 1);
let r2 = Either.of(1.null).map((x) = > x + 1);
console.log(r1); // Either {_left: 5, _right: 7}
console.log(r2); // Either {_left: 2, _right: null}
function parseJSON(str) {
try {
return Either.of(null.JSON.parse(str));
} catch (e) {
return Either.of(e.message, null); }}let r = parseJSON("{name: zs}");
console.log(r); // Either { _left: 'Unexpected token n in JSON at position 1', _right: null }
Copy the code
(5) IO functor
The _value in the IO functor is a function, and we’re treating the function as a value. IO functors can store impure actions in _value, delaying the execution of the nonexistent operation (lazy execution). Impure operations are left to the caller.
Code examples:
/ / IO functor
const fp = require("lodash/fp");
class IO {
static of(value) {
return new IO(function () {
return value;
});
}
constructor(fn) {
this._value = fn;
}
map(fn) {
return new IO(fp.flowRight(fn, this._value)); }}/ / test
// When executing the current file on the console using the node command, we can call the node method to get the path of the node file
let r = IO.of(process).map((p) = > p.execPath);
console.log(r); // IO {_value: [Function (anonymous)]} <= this anonymous Function is flowRight
// We manually call an impure function stored in _value
console.log(r._value()); // Mac output: /usr/local/bin/node; Window output: C:\Program Files\nodejs\node.exe
Copy the code
(6) Task functor in Folktale
Functors can also be used to handle asynchronous tasks, and since async is prone to callback hell, we can use task functors to do this. Because the implementation of asynchronous tasks is too complex, we use the Task functor from Folktale to demonstrate this.
- folktale
Folktale is a standard functional programming library featuring:
- Unlike Lodash and Ramda, it doesn’t offer many features;
- He provides only a few functionally handled operations, such as compose, Curry, etc., and some functor tasks, Either, MayBe, etc.
Code examples:
// The compose and curry functions in folktale are now represented
const { compose, curry } = require("folktale/core/lambda");
const { toUpper, first } = require("lodash/fp");
// The first argument to the curry function is the second argument -- the number of arguments to the function (arity).
let f = curry(2.(x, y) = > {
return x + y;
});
console.log(f(1.2)); / / 3
console.log(f(1) (2)); / / 3
let g = compose(toUpper, first);
console.log(g(["one"."two"])); // 'ONE
Copy the code
- Task functor
Here we use the Task functor from FolkTale 2.3.2 to demonstrate:
// Task handles asynchronous tasks (node, Folktale, and Lodash need to be installed in advance)
const fs = require('fs');
const {task} = require('folktale/concurrency/task');
const {split, find} = require('lodash/fp');
// Use Task to handle asynchronous file reads
function readFile(filename) {
return task(
resolver= > {
fs.readFile(filename, 'utf-8'.(err, data) = > {
if(err) resolver.reject(err); resolver.resolve(data); })}); }// Suppose there is a package.json file in the statistics directory
// Performing the above defense returns a Task functor that calls some of the built-in Task methods (e.g. Run, listen)
readFile('package,json')
.run()
.listen({
onRejected: err= > {
console.log(err);
},
onResolved: value= > {
console.log(value); }})// Execute the above code to output the following result (assuming the contents of package.json are as follows) :
/ * {" name ":" juejin "version" : "1.0.0", "description" : ""," main "is:" index. Js ", "scripts" : {" test ", "echo \" Error: no test specified\" && exit 1" }, "author": "ColdCoder" "license": "ISC", "dependencies": { "folktale": "^2.3.2", "lodash": "^4.17.21"} */
// Requirement: split the result of reading the file into an array by line, and then find the item with version
readFile('package,json')
.map(split('\n'))
.map(find( x= > x.includes('version')))
.run()
.listen({
onRejected: err= > {
console.log(err);
},
onResolved: value= > {
console.log(value); }})// The output is: "version": "1.0.0",
Copy the code
(7) the conservative functor
A conservative functor is a functor that implements the of static method.
The of method is used to avoid using new to create objects. The deeper meaning of the of method is that it is used to put values in Context (that is, to put values in containers so that values can be processed using map methods).
Code examples:
class Container {
static of(value) {
return new Container(value);
}
constructor(value) {
this._value = value;
}
map(func) {
return Container.of(func(this._value));
}
}
Container.of(2).map(x= > x + 5);
Copy the code
(8) Monad functor
- IO functor problem
If we use IO functors, we may need to execute the function in the instance several times to output the result due to the nesting of functions. Such chain calls are unpleasant to use.
// IO functor problem
const fs = require("fs");
const fp = require("lodash/fp");
class IO {
static of(value) {
return new IO(function () {
return value;
});
}
constructor(fn) {
this._value = fn;
}
map(fn) {
return new IO(fp.flowRight(fn, this._value)); }}// readFile is a nested function
let readFile = function (filename) {
return new IO(function () {
return fs.readFileSync(filename, "utf-8");
});
};
let print = function (x) {
return new IO(function () {
console.log(x);
return x;
});
};
let cat = fp.flowRight(print, readFile);
// IO(IO(x))
let r = cat("package.json");
console.log(r); // IO{_value: [Function]}
r = cat("package.json")._value();
console.log(r); // IO{_value: [Function]} IO{_value: [Function]}
// We need multiple calls before we get the result, which is cumbersome, so we need to use Monad functors to solve this problem.
r = cat("package.json")._value()._value();
console.log(r); // Package. json contents
Copy the code
So we need to use Monad functors to solve this problem.
- Monad functor
Monad functors are conservative functors that can be flattened (flattened). A functor is a Monad functor if it has both join and of methods and follows some laws.
Problems like the IO functor above that require multiple calls can be solved by using the join method below
// IO Monad
const fs = require("fs");
const fp = require("lodash/fp");
class IO {
static of(value) {
return new IO(function () {
return value;
});
}
constructor(fn) {
this._value = fn;
}
map(fn) {
return new IO(fp.flowRight(fn, this._value));
}
join() {
return this._value();
}
flatMap(fn) {
return this.map(fn).join(); }}let readFile = function (filename) {
return new IO(function () {
return fs.readFileSync(filename, "utf-8");
});
};
let print = function (x) {
return new IO(function () {
console.log(x);
return x;
});
};
/ / when used
let r = readFile('package.json').flatMap(print).join(); // Output file information
console.log(r);
// Prints the file contents in uppercase
let rUpper = readFile('package.json')
.map(x= > x.toUpperCase())
.flatMap(print)
.join();
console.log(rUpper);
// If lodash is used, the code is as follows:
// let rUpper = readFile('package.json').map(fp.toUpper).flatMap(print).join();
Copy the code
That concludes my brief introduction to the functional programming paradigm. Please do not hesitate to correct any mistakes.