Functional programming
1. The use of functional programming in front-end frameworks has gained increasing attention with the popularity of React. Vue3 is also starting to embrace functional programming. For example, if you want to build something like this, you can use tree shaking to filter useless code for testing and parallel processing. Lodash, underscore, Ramda
The concept of functional programming
1. Common Programming paradigms (Programming ideas)
Oriented Programming (POP) Object-oriented Programming (OOP) Functional programming (FP)
2. Differences between different programming paradigms
2.1 Thinking Mode Level Process-oriented programming (POP) : Simple understanding is to achieve the desired function step by step according to the steps. Object Oriented programming (OOP) : Abstracts things in the real world into classes and objects in the program world, demonstrating the relationships between things through encapsulation, inheritance, and polymorphism. Functional programming (FP) : Abstraction of things and their relationships from the real world into the program world (abstraction of operational processes)
- The essence of a program: a function that performs some operation to obtain the corresponding output from the input
- X -> f -> y, y = f(x)
- In functional programming, the function is not a function (method) in a program, but a function (mapping) in mathematics, such as y=sin(x), the relationship between x and y
- The same input always yields the same output (pure function)
- Functional programming is used to describe the mapping between data () functions, or the abstraction of operations
// Non-functional
let a = 1
let b = 2
let sum = a + b
/ / function type
function add(a, b) {
return a + b
}
let sum = add(a, b)
Copy the code
Advantages of functional programming 3.1 Encapsulated functions can be reused to reduce the amount of code 3.2 Functions abstracted from functional programming are mostly fine-grained functions, which can be combined to form powerful functions
3. Functional programming concepts
1. How to understand that functions are first-class citizens?
MDN First-class Function
- Functions can be stored in variables
- Functions can be arguments
- A function can be a return value
In JavaScript, a function is an ordinary object (which can be defined by new function). Because it’s just a normal object, it stores it in a variable. Because it is a normal object, it can be used as a parameter or return value of another function.
Demonstration: Assigning a function to a variable
// Assign a function to a variable
var fn = function () {
console.log('hello First-class Function')
}
fn()
// An example
var BlogController = {
index (posts) { return Views.index(posts) },
show (post) { return Views.show(post) },
create (attrs) { return Db.create(attrs) },
update (post, attrs) { return Db.update(post, attrs) },
destroy (post) { return Db.destroy(post) }
}
// Optimized example
var BlogController = {
index: Views.index,
show: Views.show,
create: Db.create,
update: Db.update,
destroy:Db.destroy
}
// Parsing: the scenario is when one function wraps another function and they have the same form.
// Assign a function or method with the same name and argument to another function or method with the same name and argument
Copy the code
2. What is a higher-order function?
Higher-order functions
- You can pass a function as an argument to another function
- You can treat a function as the return result of another function
3 · Higher-order functions – functions as arguments
Example: Demonstrates passing a function as an argument to another function
let arr = [1.3.4.7 ,8]
// simply simulate forEach
function forEach(array, fn) {
for (let i = 0; i < array.length; i++) {
fn(array[i])
}
}
/ / test forEach
forEach(arr, function(item) {
console.log(item)
})
// Simple simulation filter
function filter(array, fn) {
let result = []
for (let i = 0; i < array.length; i++) {
if(fn(array[i])) {
result.push(array[i])
}
}
return result
}
/ / filter test
let r = filter(arr, function(item) {
return item % 2= = =0
})
console.log(r)
Copy the code
Conclusion: Passing functions as arguments makes them more flexible
4 · Higher-order functions – Functions as return values
Syntax: Treat a function as the return value of another function
function makeFn() { // Define the function
const msg = 'hello function'
return function () {
console.log(msg)
}
}
makeFn()() / / execution
Copy the code
Demo: A meaningful function encapsulation, once scenario: when the user is paying for an order, the function is required to be executed only once
function once(fn) {
let done = false // marks whether the passed function was executed
return function() { // function as return value
if(! done){ done =true
return fn.apply(this.arguments) // arguments refer to pass-arguments to an anonymous function of return}}}let pay = once(function(money) {
console.log(` pay:${money} RMB`)
})
pay(5)
pay(5)
pay(5)
Copy the code
5. Higher order functions – Meaning of use
First, define the core idea of functional programming: abstract the operation process as a function, and use higher-order functions everywhere.
- Abstraction can help us mask the details and focus only on our goals
- Higher-order functions are used to help us abstract away some general problems
Examples are easy to understand:
// Process oriented approach
let array = [1.2.3.4]
for (let i = 0; i < array.length; i++) {
console.log(array[i])
}
// Higher-order higher-order functions
let array = [1.2.3.4]
forEach(array, item= > { // Use abstract functions to mask the details and focus only on the target
console.log(item)
})
let r = filter(array, item= > {
return item % 2= = =0
})
Copy the code
6. Commonly used higher-order functions
ForEach, map, filter, every, some, find/findIndex, reduce, sort…
/ / map
const map = (arr, fn) = > {
let results = []
for (value of arr) {
results.push(fn(value))
}
return results
}
/ / test
// let arr = [1, 2, 3]
// arr = map(arr, v => v * v)
// console.log(arr)
Every / / simulation
const every = (arr, fn) = > {
let result = true
for (value of arr) {
result = fn(value)
if(! result)break
}
return result
}
/ / test
// let arr = [15, 11]
// let r = every(arr, v => v > 10)
// console.log(r)
/ some/simulation
const some = (arr, fn) = > {
let result = false
for (value of arr) {
result = fn(value)
if(result) break
}
return result
}
/ / test
// let arr = [1, 2, 9, 7]
// let r = some(arr, v => v % 2 === 0)
// console.log(r)
Copy the code
Conclusion: By passing one function as an argument to another, you can make the other function more flexible.
7. Closure concept
- Closures: A function is bundled together with references to its surrounding state (lexical environment) to form a closure
- Features: You can call an inner function of a function from another scope and access members of that function’s scope
7.1 Basic Examples of closures:
function makeFn() { // Define the function
const msg = 'hello function'
return function () {
console.log(msg)
}
}
const fn = makeFn()
fn()
Copy the code
- The core function of the closure is to extend the scope of the MSG member in the makeFn scope of the outer function.
MakeFn () normally frees up memory for MSG after execution. But because MSG is referenced in the returned internal function, the memory occupied by MSG is not freed after makeFn() is executed.
7.2 Closure Application Examples
function once(fn) {
let done = false // marks whether the passed function was executed
return function() { // function as return value
if(! done){ done =true
return fn.apply(this.arguments) // arguments refer to pass-arguments to an anonymous function of return}}}let pay = once(function(money) {
console.log(` pay:${money} RMB`)
})
pay(5)
pay(5)
Copy the code
The essence of closures: functions are placed on an execution stack at execution time and removed from the stack when the function completes execution, but the scoped members on the heap cannot be freed because they are referenced externally, so the inner function can still access the members of the external function. When once() is executed, it is removed from the execution stack, but the done variable defined in once is referenced in pay, so it cannot free done memory in the heap.
8. Closure case
8.1 Case 1: Encapsulate the power power function of number and analyze its execution stack and scope in the browser
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title>Document</title>
</head>
<body>
<script>
function makePow(power) {
return function(number) {
return Math.pow(number, power)
}
}
let power2 = makePow(2)
let power3 = makePow(3)
console.log(power2(2))
console.log(power2(4))
console.log(power3(3))
</script>
</body>
</html>
Copy the code
When line 20 is executed, the code in the script tag is actually called from an anonymous function. There are two scopes in the Scope Scope, one is the Scope of the variable declared by the LET and the other is the global Scope.
2. At line 21, there are two anonymous functions in the call stack, one is script and the other is the anonymous function returned from makePow. There are four scopes in Scope: local Scope, variables in closure, Scope of let declared variables, and global Scope. The power variable is therefore accessible in the anonymous function returned from the makePow function, with a value of 2. The final print is 4
Functional programming – pure functions
1. Pure functions
Concept: Pure functions are the core of functional programming, and functions in functional programming are pure functions. Pure functions: Two necessary conditions must be met identical inputs always get the same outputs without any observable side effects
- A pure function is like a function in mathematics, y = f(x).
- Lodash is a library of pure functions that provides methods for operating on arrays, numbers, objects, strings, functions, and more
2. Pure and impure functions
Arrays of slice and splice are pure and impure functions, respectively
let numbers = [1.2.3.4.5]
/ / pure functions
numbers.slice(0.3)// => [1, 2, 3]
numbers.slice(0.3)// => [1, 2, 3]
numbers.slice(0.3)// => [1, 2, 3]
// Impure function (do not satisfy the same input will always get the same output)
numbers.splice(0.3)// => [1, 2, 3]
numbers.splice(0.3)/ / = > [4, 5]
numbers.splice(0.3)/ / = > []
Copy the code
3. Custom pure functions
// Custom pure functions
function sum (n1, n2) {
return n1 + n2
}
console.log(sum(1 + 2)) / / 3
console.log(sum(1 + 2)) / / 3
console.log(sum(1 + 2)) / / 3
Copy the code
Analyze the process of using custom pure functions:
- Phenomenon: Functional programming does not preserve the results of a calculation, so variables are immutable (stateless)
- Conclusion: So we can hand off the results of one function to another.
4. Some methods in Lodash
/ / demo lodash
// first / last / toUpper / each / includes / find /findIndex
const _ = require('lodash')
const array = ['jack'.'tom'.'lucy'.'kate']
console.log(_.first(array))
console.log(_.last(array))
console.log(_.toUpper(_.first(array)))
console.log(_.reverse(array))
_.forEach(array, (item, index) = > {
console.log(item, index)
})
Copy the code
5. Benefits of pure functions
5.1. Cacheable
- Because pure functions always have the same result on the same input, the result of a pure function can be cached.
- Because there are some time-consuming and complex pure functions, the results can be cached to improve performance
// Memory function in lodash
const _ = require('lodash')
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
Copy the code
Simulate a memoize function yourself
function getArea (r) {
console.log(r)
return Math.PI * r * r
}
/ / simulation memoize
function memoize (f) {
let cache = {}
return function () {
let key = JSON.stringify(arguments)
cache[key] = cache[key] || f.apply(f, arguments)
return cache[key]
}
}
let getAreaWithMemory = memoize(getArea)
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
Copy the code
5.2 testable because pure functions always have inputs and outputs, unit tests assert the results of function execution. 5.3 Parallel processing scenario: When operating shared memory data (such as global variables) at the same time in a multi-threaded environment, accidents may occur and the final result is uncertain.
A pure function is a closed space. Pure functions do not need to access the shared data and only depend on the parameters passed in, so they can be run arbitrarily in parallel.
While JavaScript is single-threaded, ES6 adds an object, the Web Worker. So after ES6, we can turn on multi-threaded execution to improve performance (not often).
6. Side effects
Pure functions: The same input always yields the same output, without any examples of observable side effects to deepen understanding:
/ / not pure
let mini = 18
// Return result of checkAge function, affected by global variable mini (so-called side effect)
function checkAge (age) {
return age >= mini
}
// Pure (hardcoded, later can be solved by Currization)
function checkAge (age) {
let mini = 18
return age >= mini
}
Copy the code
A pure function returns the same output based on the same input. A side effect can occur if the function depends on an external state that cannot guarantee the same output.
7. Sources of side effects
- The configuration file
- The database
- Get user input
- .
Conclusion: 1, all the external interaction may cause side effects, side effects can make the method of common descent, not suitable for reuse and extend 2, side effects may also come to the program must be safe hidden trouble, such as the external interactive user input may be cross-site scripting attacks) 3, side effects cannot be completely banned, but should as far as possible control they occur in the controllable range
5. Corrification
1. Previously, when considering the side effects of functions, the following scenarios were encountered:
// Pure function (hard coded, will be solved by Currization)
function checkAge (age) {
let mini = 18
return age >= mini
}
Copy the code
2. After transformation, it becomes a common pure function
function checkAge (min, age) {
return age >= min
}
console.log(checkAge(18.20))
console.log(checkAge(18.22))
console.log(checkAge(18.24))
Copy the code
Analysis of the above code has the following scenarios: function calls based on age 18 are often used, and the base value min = 18 needs to be defined repeatedly.
Solve scenario problems with closures and higher-order functions
// function checkAge (min) {
// return function (age) {
// return age >= min
/ /}
// }
ES6 implements the above functions
let checkAge = min= > (age= > age >= min)
// Base values are defined only once in closures
let checkAge18 = checkAge(18)
let checkAge20 = checkAge(20)
console.log(checkAge18(20))
Copy the code
4. Summarize the concept of Function Coriolization:
When a function has multiple arguments, such as checkAge (min, age), the closure is used in combination with higher-order functions to call a function that passes only part of the argument (the argument will never change, such as: 18) and returns a function that accepts the remaining arguments and returns the corresponding result.
5. The Cremation method in Lodash
_. Curry (func) pure functions
- Function: creates a function that takes one or more arguments from func, executes func and returns the result of execution if all the arguments needed by func are provided, otherwise returns the function and waits for the rest of the arguments to be received.
- Parameters: functions that require currization
- Return value: The currified function
const _ = require('lodash')
// The function needs to be currified
function sum (a, b, c) {
return a + b + c
}
// The currie function
let curried = _.curry(sum)
console.log(curried(1.2.3)) / / 6
console.log(curried(1.2) (3)) / / 6
console.log(curried(1) (2) (3)) / / 6
// The curry function can convert any multivariate function into a unary function
Copy the code
6. The Case of Corrification
6.1, the functional requirements: extract the blank characters in the string | digital
// ''.match(/\s+/g)
// ''.match(/\d+/g)
// Define pure functions
function match (reg, str) {
return str.match(reg)
}
Copy the code
Problem: There are regs that are redefined repeatedly when the match pure function is often used to extract specified characters.
6.2. How to solve the problem? Solution: To solve the problem by using function Coriolization
const _ = require('lodash')
To avoid declaring variables, pass the pure function anonymously to Curry
const match = _.curry(function (reg, str) {
return str.match(reg)
})
// The new function is generated by the function Currie
const haveSpace = match(/\s+/g)
const haveNumber = match(/\d+/g)
/ / test
console.log(haveSpace('hello world')) // [' ']
console.log(haveNumber('1234abc')) / / / '1234'
console.log(haveNumber('abc')) // null
Copy the code
6.3 Functional requirements: Find elements with whitespace characters in the array
const _ = require('lodash')
To avoid declaring variables, pass the pure function anonymously to Curry
const match = _.curry(function (reg, str) {
return str.match(reg)
})
// The new function is generated by the function Currie
const haveSpace = match(/\s+/g)
const haveNumber = match(/\d+/g)
// Define the pure function of the Lodash function
const filter = _.curry(function (func, array) {
return array.filter(func)
})
// Pass two arguments to the currified pure function
console.log(filter(haveSpace, ['John Connor'.'Tom'])) // [ 'John Connor' ]
console.log(filter(haveSpace, ['Hello World'.'Kate'])) // [ 'Hello World' ]
Copy the code
Problem: haveSpace needs to be passed on every call and should be solidified as a function for specific requirements.
6.4. How to solve the problem? Scheme: Pass only one parameter (simply understood as a functional parameter) to the pure function filter of Currization and redefine it.
const _ = require('lodash')
To avoid declaring variables, pass the pure function anonymously to Curry
const match = _.curry(function (reg, str) {
return str.match(reg)
})
// The new function is generated by the function Currie
const haveSpace = match(/\s+/g)
const haveNumber = match(/\d+/g)
// Define the pure function of the Lodash function
const filter = _.curry(function (func, array) {
return array.filter(func)
})
const findSpace = filter(haveSpace)
console.log(findSpace(['John Connor'.'Tom'])) // [ 'John Connor' ]
console.log(findSpace(['Hello World'.'Kate'])) // [ 'Hello World' ]
Copy the code
Review the use of the Curry method in Lodash
const _ = require('lodash')
// The function needs to be currified
function sum (a, b, c) {
return a + b + c
}
// The currie function
let curried = _.curry(sum)
console.log(curried(1.2.3)) / / 6
console.log(curried(1.2) (3)) / / 6
console.log(curried(1) (2) (3)) / / 6
// The curry function can convert any multivariate function into a unary function
Copy the code
8. Simulate the curry method
function curry (func) {
return function curriedFn(. args) {
// Determine whether the number of arguments (args.length) is less than the number of parameters (func.length)
// If the condition is not met, the parameters required by func are met. args)
if (args.length < func.length) {
return function () {
// args is a variable in a closure that holds the last parameter passed to the function
// arguments are the arguments passed to this call
// The inner function needs to call the outer function, passing in the accumulated arguments after merging
// Call the outer function curriedFn to make it easier to call
returncurriedFn(... args.concat(Array.from(arguments)))}}returnfunc(... args) } }// Test function (pass)
let curried = curry(sum)
console.log(curried(1.2.3)) / / 6
console.log(curried(1.2) (3)) / / 6
Copy the code
9. Corrified summary
- Currization allows us to pass fewer arguments to a function, resulting in a new function that already remembers some of the fixed arguments. This is a cache of function parameters.
Examples to deepen understanding:
const _ = require('lodash')
To avoid declaring variables, pass the pure function anonymously to Curry
const match = _.curry(function (reg, str) {
return str.match(reg)
})
// Create a new function with some fixed parameters in mind by using the function Currie
const haveSpace = match(/\s+/g)
const haveNumber = match(/\d+/g)
Copy the code
- Make functions more flexible and smaller in granularity
Example:
// Define more fine-grained functions based on the example above
// Define the pure function of the Lodash function
const filter = _.curry(function (func, array) {
return array.filter(func)
})
const findSpace = filter(haveSpace)
Copy the code
- You can convert multivariate functions into unary functions, and you can combine functions to produce powerful functions
6. Function combination
1. Problems solved by function combination
1.1. Problem description
Pure functions and Currization are easy to write onion code h(g(f(x)).
1.2. Example Scenario
Gets the last element of the array and converts it to uppercase
_.toUpper(_.first(_.reverse(array)))
Copy the code
Function composition allows us to recombine fine-grained functions to generate a new function
2. Preset concept – pipeline
/ / pseudo code
fn = compose(f1, f2, f3)
b = fn(a)
Copy the code
3. Function combination
- Definition: function composition: If a function needs to be processed by multiple functions to get the final value, the intermediate functions can be combined into a single function
- A function is like a pipeline of data, and a combination of functions connects these pipelines to make data pass through multiple pipelines to form the final result
- Execution order: Function combinations are executed from right to left by default
function composition (f, g) {
return function (value) {
return f(g(value))
}
}
reverse = array= >array.reverse()
first = array= > array[0]
const last = composition(first, reverse)
console.log(last([1.2.3.4])) / / 4
Copy the code
4. Use of combinatorial functions in Lodash
- Lodash combines the flow() or flowRight() functions, both of which can combine multiple functions
- Low () runs from left to right
- FlowRight () runs from right to left (more often)
// The use of combinatorial functions
const _ = require('lodash')
const toUpper = s= > s.toUpperCase()
const reverse = arr= > arr.reverse()
const first = arr= > arr[0]
const f = _.flowRight(toUpper, first, reverse)
console.log(f(['one'.'two'.'three']))
Copy the code
5. Simulation of combinatorial functions in Lodash
Simulation of flowRight combination function
const toUpper = s= > s.toUpperCase()
const reverse = arr= > arr.reverse()
const first = arr= > arr[0]
// The flowRight combination function is simulated
// function compose (... args) {
// return function (value) {
// // executes functions from right to left, passing in the result as an argument to the next function,
// // returns the result
// return args.reverse().reduce(function (acc, fn) {
// return fn(acc)
}, value) // Sets the initial value to the parameter passed in
/ /}
// }
const compose = (. args) = > value= > args.reverse()
.reduce((acc, fn) = > { return fn(acc) }, value)
const f = compose(toUpper, first, reverse)
console.log(f(['one'.'two'.'three'])) // THREE
Copy the code
6. Function combination – associative law
The combination of functions must satisfy associativity:
- We can either combine g with H, or f with g, and it’s the same thing
// Associativity
let f = compose(f, g, h)
let associative = compose(compose(f, g), h) == compose(f, compose(g, h))
// true
Copy the code
So the code could also look like this:
const _ = require('lodash')
// const f = _.flowRight(_.toUpper, _.first, _.reverse)
// const f = _.flowRight(_.flowRight(_.toUpper, _.first), _.reverse)
const f = _.flowRight(_.toUpper, _.flowRight(_.first, _.reverse))
console.log(f(['one'.'two'.'three'])) // => THREE
Copy the code
7. Function combination – debugging
How to debug composite functions:
const _ = require('lodash')
const trace = _.curry((tag, v) = > {
console.log(tag, v)
return v
})
const split = _.curry((sep, str) = > _.split(str, sep))
const join = _.curry((sep, array) = > _.join(array, sep))
const map = _.curry((fn, array) = > _.map(array, fn))
const f = _.flowRight(join(The '-'), trace('after the map'), map(_.toLower),
trace('after the split), split(' '))
console.log(f('NEVER SAY DIE'))
Copy the code
Problem: The _. Split, _. Join, and _. Map methods defined in Lodash, while commonly used, are not suitable for use in function combinations and require currification if they are used at all. Because you need unary functions in a combination of functions
8. Function combination-Lodash-FP module
8.1 how to solve the problems in function combination debugging: using lodash-FP module 8.2, Lodash-FP module
- The Lodash-FP module provides a practical FP (Functional programming) module that is friendly to Functional programming
- Immutable auto-curried iteratee-first data-last method is provided
/ / lodash module
const _ = require('lodash')
_.map(['a'.'b'.'c'], _.toUpper) // => ['A', 'B', 'C']
_.map(['a'.'b'.'c']) // => ['a', 'b', 'c']
_.split('Hello World'.' ')
/ / lodash/fp module
const fp = require('lodash/fp')
fp.map(fp.toUpper, ['a'.'b'.'c'])
fp.map(fp.toUpper)(['a'.'b'.'c'])
fp.split(' '.'Hello World')
fp.split(' ') ('Hello World')
Copy the code
8.3. Transform function combination – debug code
const fp = require('lodash/fp')
const f = fp.flowRight(fp.join(The '-'), fp.map(_.toLower), fp.split(' '))
console.log(f('NEVER SAY DIE'))
Copy the code
9. Small problems with the Lodash-Map method
9.1. Differences between map methods in LoDash/FP module and LoDash module
const _ = require('lodash')
console.log(_.map(['22'.'6'.'10'].parseInt)) // [ 22, NaN, 2 ]
// parseInt('22', 0, ['22', '6', '10'])
// parseInt('5', 0, ['22', '6', '10'])
// parseInt('10', 0, ['22', '6', '10'])
const fp = require('lodash/fp')
console.log(fp.map(parseInt['22'.'6'.'10'])) // [22, 6, 10]
Copy the code
Functor(Functor)
Functor (Functor)
So far, I’ve learned some of the basics of functional programming, but I haven’t shown you how to keep side effects under control, handle exceptions, and operate asynchronously in functional programming. Functors solve these problems. A functor is a container that contains values and their deformation relationships (the deformation relationships are functions). The container is implemented through a plain object with a map method that runs a function to process the values.
Ordinary functor
According to the description of functor, it needs to satisfy two points:
- Maintains a value internally and only operates within the container (like a private property)
- There is a
map
Methods can be directed tomap
Method to modify the value and pass the new value to the new container
Implement this functor as described above:
class Container {
// Define a static method that returns an instance of the class. Avoid using new externally to create objects
static of (value) {
return new Container(value)
}
// Define a private attribute _value in the constructor, allowing operations only inside the container
constructor(value) {
this._value = value
}
The map method modifies the value of the private _value attribute by passing in the function fn
map (fn) {
return Container.of(fn(this._value))
}
The join method returns the value inside the function
join () {
return this._value
}
}
// Use loadsh/fp module test, requirement: get the uppercase of the last element of the array
const fp = require('lodash/fp')
const getFirst = array= > array[0]
let r = Container.of(['aa'.'bb'.'cc'])
.map(fp.reverse)
.map(getFirst)
.map(fp.toUpper)
console.log(r.join()) // 'CC'
Copy the code
The functor itself is a container, and the new value flows to the next container after fn processes the data into the container. However, there is a lack of exception handling, if one of the functors processing problems, how to deal with? How does the program work properly? There are a lot of exceptions, first for the external passed null (undefined) case, it is the turn of the MayBe functor.
MayBe functor
The Maybe functor is used to handle incoming null values (within permissible limits).
class Maybe {
static of (value) {
return new Maybe(value)
}
constructor(value) {
this._value = value
}
map (fn) {
return this.isEmpty() ? Maybe.of(null) : Maybe.of(fn(this._value))
}
isEmpty () {
return this._value === null || this._value === undefined
}
join () {
return this._value
}
}
let r = Maybe.of('aaa')
.map(x= > x.toUpperCase())
.map(x= > null)
.map(x= > x.split(' '))
console.log(r.join()) // null
Copy the code
Maybe will return null for null and fn processing will not cause exceptions. But a new, new question arises, how do we determine which step produces a null value? How do I catch an exception inside a function? It’s the turn of the Either functor…
Either functor
The Either functor consists of Left and Right functors, similar to if/else logic. When an exception occurs on a functor, the Left functor stores the value of the exception and returns the current functor. The Left functor does not continue to pass the value itself, and the Right functor returns the new functor as normal.
// Handle the exception functor, return its own instance, no new functor
class Left {
static of (value) {
return new Left(value)
}
constructor(value) {
this._value = value
}
map () {
return this
}
join () {
return this._value
}
}
// Normal functor, return new functor normally
class Right {
static of (value) {
return new Right(value)
}
constructor(value) {
this._value = value
}
map (fn) {
return Right.of(fn(this._value))
}
join () {
return this._value
}
}
Copy the code
When we try to parse a JSON string, an exception is bound to occur if the string is passed in the wrong format. You can use Either functors to handle exceptions.
function parseJSON (str) {
try {
return Right.of(JSON.parse(str))
} catch (e) {
return Left.of({ error: e.message })
}
}
let l = parseJSON("{ name: tom }")
.map(x= > x.name.toUpperCase())
console.log(l.join()) // { error: 'Unexpected token n in JSON at position 2' }
let r = parseJSON('{ "name": "tom" }')
.map(x= > x.name.toUpperCase())
console.log(r.join()) // TOM
Copy the code
IO functor
The MayBe and Either functors we have used so far handle exceptions generated during the execution of a function. But if fn is passed as an impure function, it will have an affected value and have side effects. This is unavoidable, so you can store the impure action in _value, delay the execution of the impure action, and finally hand the impure action over to the caller (similar to “flipping the pan”).
const fp = require('lodash/fp')
class IO {
static of (x) {
return new IO(() = > x)
}
constructor(fn) {
this._value = fn
}
map (fn) {
return new IO(fp.flowRight(fn, this._value))
}
join () {
return this._value()
}
}
/ / test
let r = IO.of([1.2.3])
.map(fp.reverse)
.map(fp.first)
console.log(r.join()) / / 3
Copy the code
In the test code, we passed an array to the IO functor, but it was converted into a function inside the functor and stored in this._value. Fp. flowRight was used for function combination. We only get the return value when we call join(), which ensures lazy evaluation until we need it. We guarantee that the procedure is pure, leaving the impure operations in the combination of functions to the caller.
Folktale – Task functor
So far, we have focused on exception handling in FP and how to control side effects. We can use the folktable library Task functor to help us deal with asynchronous tasks.
Folktale is a standard functional programming library. Unlike Lodash and Ramda, folktale does not provide many functional functions. Instead, it only provides some functional operations, such as compose, Curry, and some functor tasks, Either, MayBe, etc
For asynchronous operations that read files in the Node environment, we can use Task functors to handle success and failure of reads.
const { task } = require('folktale/concurrency/task')
const fs = require('fs')
const fp = require('lodash/fp')
function readFile (filename) {
return task((resolver) = > {
fs.readFile(filename, 'utf-8'.(err, data) = > {
if (err) {
resolver.reject(err)
}
resolver.resolve(data)
})
})
}
readFile('package.json')
.map(fp.split('\n'))
.map(fp.find((x) = > x.includes('version')))
.map(fp.toUpper)
.run()
.listen({
onRejected: (err) = > {
console.log(err)
},
onResolved: (value) = > {
console.log(value)
}
})
Copy the code
The Task functor is a functor in FP that handles asynchronous operations, much like the promise operation.
Monad functor
The IO functor above has some problems as follows:
const fp = require('lodash/fp')
const fs = require('fs')
class IO {
static of (x) {
return new IO(() = > x)
}
constructor(fn) {
this._value = fn
}
map (fn) {
return new IO(fp.flowRight(fn, this._value))
}
join () {
return this._value()
}
}
let readFile = function (filename) {
return new IO(() = > {
return fs.readFileSync(filename, 'utf-8')})}let print = function (x) {
return new IO(() = > {
console.log(x)
return x
})
}
let cat = fp.flowRight(print, readFile)
// IO(IO(x)) has a problem :IO nested API calls are not elegant
let r = cat('package.json')
console.log(r.join())
// IO { _value: [Function] }
// IO { _value: [Function] }
Copy the code
Why print functor twice after join but not file content? When cat(‘package.json’) is run, the function composition and functor values are set. Implement the readFile, Return new IO(() => {return fs.readfilesync (filename, Return fs.readfilesync (filename, ‘utf-8’)}, New IO(() => {return fs.readfilesync (filename, ‘utf-8’)}) to print. Print sets its functor to the same value
this._value = () = > {
console.log(new IO(() = > fs.readFileSync(filename, 'utf-8')))
return new IO(() = > fs.readFileSync(filename, 'utf-8'))}Copy the code
Then return a nested functor where f is:
new IO(() = > {
console.log(new IO(() = > fs.readFileSync(filename, 'utf-8')))
return new IO(() = > fs.readFileSync(filename, 'utf-8'))})Copy the code
When the join method is executed, the following functions are executed:
() = > {console.log(new IO(() = > fs.readFileSync(filename, 'utf-8')));
return new IO(() = > fs.readFileSync(filename, 'utf-8'))}Copy the code
The first print is done, and then the function returns a function, so we can use console to print the returned functor. The functor that we actually want to execute is still not executed, so we need to call join again on the returned functor to read the file:
console.log(f.join().join()) // File contents
Copy the code
We can see how complicated the code gets when functors are nested within functors, so we need to implement a flatMap method inside functors to “flatten” them.
flatMap (fn) {
return this.map(fn).join()
}
Copy the code
The modified code is as follows:
let r = readFile('package.json')
.map(fp.toUpper)
.flatMap(print)
.join()
Copy the code
When a function returns a functor, we should think of using Monad functors to implement methods like flatMap inside the functor. When a function returns a value, we can use the map method. Can solve the problem very well.
Summary of functional programming
Functional programming deals with the basic theory of functions: functions are first-class citizens, higher-order functions, closures. On this basis, we extend the knowledge of village function, function currization and function combination. In order to ensure that side effects can be controlled, exceptions handled and async handled correctly in functional programming, various functional functors are introduced. The idea of functional programming is to abstract the process, which can improve the quality of our code, reduce the amount of code, and avoid defects (side effects).
reference
ECMAScript2015~2020 Syntax full parsing arrow functions for functional programming is an introduction to functional programming