Functional programming

1.1 Functional programming advantages

  • You don’t have to use this
  • The packaging process can use Tree Shaking to filter out useless code
  • Convenient testing and parallel processing

1.2 Functional programming concepts

  • Functional programming, namely Functional Programing, is one of the programming paradigms, and process – oriented programming, object-oriented programming for parallel relations.

  • Functional programming is used to describe the mapping between data

The origin of functional programming was a branch of mathematics called Category Theory.

The key to understanding functional programming is to understand category theory. It is a very complex mathematics, which holds that all the conceptual systems in the world can be abstracted into categories.

“Categories are objects connected by arrows.” (In mathematics, a category is an algebraic structure that comprises “objects” that are linked by “arrows”.)

That is to say, concepts, things, objects, etc., which have a certain relationship with each other, constitute “categories”. Anything can define a category if it can find a relationship between them.

Category theory is a higher level of set theory abstraction, simple understanding is “set + function”.

We can think of the category as a container containing two things.

  • Value (value)
  • The deformation of the values, that's the function.

In functional programming, a function is a pipe. One value goes in here, a new value comes out there, nothing else.

Functional programming has two basic operations: composition and keriification.

If a value passes through multiple functions to become another value, you can combine all the intermediate steps into a single function, which is called "compose of functions."

The composition of functions must also satisfy the associative law.

F (x) and g(x) are combined to form f(g(x)), with the hidden premise that both f and g can accept only one parameter. If you can accept multiple arguments, such as f(x, y) and g(a, b, c), function composition is cumbersome.

That’s where the currization of the function comes in. The so-called “Coriolization” is to transform a function with many parameters into a function with one parameter.

With The Currization, we can do that, all functions take one argument. Unless otherwise noted, the default function takes only one argument, which is the value to be processed.

1.3 Higher-order functions

Functions that take functions as arguments or return values, such as forEach, filter, bind, etc

/** * function as argument */
// forEach
function forEach(arr, fn) {
    for (let i = 0; i < arr.length; i ++) {
        fn(arr[i], i, arr)
    }
}
const arr = [1.2.3.4]
forEach(arr, function (item, index, itself) {
    console.log(item, index, itself)
})
Copy the code
/** * function as return value */
// once
function once(fn) {
    let flag = false
    return function () {
        if(! flag) { flag =true
            return fn.apply(this.arguments)}}}const pay = once(function (money) {
    console.log(money)
})

pay(5)
pay(5)
pay(5)
Copy the code

Higher-order functions can mask details and allow us to focus more on the goal

1.4 the closure

A closure is a function that has a reference to a member variable in another function so that it cannot be released

// Closure example
function greet() {
    const msg = 'hello'
    return function () {
        console.log(msg)
    }
}

greet()()
Copy the code
function salary(base) {
    return function (per) {
        console.log(base + per)
    }
}

const level1 = salary(10000)
const level2 = salary(20000)

level1(8000)
level2(8000)
Copy the code

1.5 pure functions

Pure functions will always have the same output for the same input

/ / pure functions
const arr = [1.2.2.3.65]
console.log(arr.slice(0.3))
console.log(arr.slice(0.3))
console.log(arr.slice(0.3))

/ / the result
// [1, 2, 2]
// [1, 2, 2]
// [1, 2, 2]

// impure function
console.log(arr.splice(0.3))
console.log(arr.splice(0.3))
console.log(arr.splice(0.3))

/ / the result
// [1, 2, 2]
// [3, 65]
/ / []
Copy the code

1.5.1 Benefits of pure functions

  • cacheable

    Because it always has the same result for the same input, it can cache the same result

    // memory function
    function memoize(fn) {
        const cache = {}
        console.log('first time')
        return function () {
            const key = JSON.stringify(arguments)
            cache[key] = cache[key] || fn.apply(fn, arguments)
            return cache[key]
        }
    }
    const r = memoize(function () {
        console.log('fn')
    })
    
    r()
    r()
    r()
    
    / / print
    // first time
    // fn
    // fn
    // fn
    Copy the code
  • testable

    Pure functions make testing easier

  • Parallel processing

    Pure functions do not need to access shared memory data

1.6 Mr Currie

When a function has multiple arguments, you can pass in some arguments and then return a new function that takes the rest and returns the result

// The original function

function checkAge(age) {
    const min = 18
    return age > min
}
console.log(checkAge(19))

/ / the result
// true

/ / curry
function checkAgeCurry(min) {
    return function (age) {
        return age > min
    }
}

const min = checkAgeCurry(18)

console.log(min(20))
console.log(min(10))

/ / the result
// true
// false

// simplify
const checkAgeSimplified = min= > age= > age > min

console.log(checkAgeSimplified(18) (20))

/ / the result
// true
Copy the code

1.6.1 Implementation of Corrification

// Simulate kerritization
// Take a function and make it currified
function curry(fn) {
    return function cu(. args) {
        // Check whether the length of the returned function is the same as the original function. Args), otherwise the argument concatenation recursively calls CU
        if (args.length < fn.length) {
            return function () {
                returncu(... args.concat(Array.from(arguments)))}}returnfn(... args) } }Copy the code

1.7 Function Combination

A function that combines multiple fine-grained functions to form a complex function

// Function combination

function compose(first, reverse) {
    return function (arr) {
        return first(reverse(arr))
    }
}

function first(arr) {
    return arr[0]}function reverse(arr) {
    return arr.reverse()
}

const findLast = compose(first, reverse)

console.log(findLast([1.2.3]))
Copy the code

1.7.1 simulation lodash/flowRight

// flowRight

// const compose = (... fns) => val => fns.reverse().reduce((pre, cur) => cur(pre), val)

function compose(. fns) {
    return function (val) {
        return fns.reverse().reduce((pre, cur) = > cur(pre), val)
    }
}

const first = arr= > arr[0]
const reverse = arr= > arr.reverse()

const findLast = compose(first, reverse)

console.log(findLast([1.2.3]))
Copy the code

1.8 functor

, version 1.8.1 Functor

Functions can be used not only to convert values within the same category, but also to convert one category into another. This is where Functor comes in.

Functors are the most important data types in functional programming, as well as the basic units of operation and function.

It is first and foremost a category, that is, a container containing values and deformation relations. In particular, its deformation relationship can be applied to each value in turn, transforming the current container into another container.

Anyone withmapThe data structure of a method can be treated as an implementation of functors.

By convention, a marker for a functor is that the container has a map method. This method maps every value in a container to another container.

// Implementation of functor
class Functor {
  constructor(val) { 
    this.val = val; 
  }

  map(fn) {
    return new Functor(fn(this.val)); }}Copy the code

In the above code, Functor is a Functor whose map method takes f as an argument and returns a new Functor containing the value that f(this.val) processed.

/ / use cases
new Functor(1).map(x= > x + 1)

// return Functor(2)
Copy the code

The above example shows that all operations in functional programming are performed by functors, that is, not directly on a value, but on the value’s container —- functor. Functors themselves have external interfaces (map methods), through which functions are operators that access the container and cause the value inside the container to be deformed.

Functional programming, in fact, is the various operations of functors. Since the operation methods can be encapsulated in functors, different types of functors are derived. There are as many kinds of functors as there are operations. Functional programming becomes using different functors to solve real problems.

1.8.2 of method

The new functor generation operation in 1.8.1 uses the object-oriented new operator, so the functional programming convention is that functors have an of method to generate new containers.

The // of method replaces the new operator
class Functor {
  static of(val) {
    return new Functor(val)
  }
    
  constructor(val) { 
    this.val = val; 
  }

  map(fn) {
    return Functor.of(fn(this.val)); }}Copy the code

Thus the use case in 1.8.1 can be modified to the following form

/ / use cases
Functor.of(1)
	.map(val= > val + 1)

// 返回结果为  Functor(2)
Copy the code

1.8.3 strategized functor

Functors accept various functions and handle values inside the container. The problem here is that the value inside the container may be a null value (such as NULL), and the external function may not have a mechanism to handle null values. If null values are passed in, an error is likely.

Functor.of(null)
    .map(str= > str.toUpperCase());
Copy the code

An error is reported when null is passed in the above code.

In the map method of the Maybe functor, null detection is set:

class Maybe extends Functor {
    map(fn) {
        return this.val ? Maybe.of(fn(this.val)) : Maybe.of(null)}}Copy the code

This way functors don’t have problems handling null values

Maybe.of(null)
	.map(str= > str.toUpperCase())

// Maybe(null)
Copy the code

1.8.4 Either functor

In functional programming, the Either functor implements if… else… This operation.

The Either functor has two internal values: Left and right. Normally, an rvalue is used, and an lvalue is used when an exception occurs.

// Right
class Right extends Functor {
    map(fn) {
        return Right.of(fn(this.val))
    }
}

// Left
class Left extends Functor {
    map(fn) {
        return this
    }
}

Right.of(1)
    .map(x= > x + 1)
Left.of(1)
	.map(x= > x + 1)

/ / the result
// Right(2)
// Left(1)
Copy the code

Another use of Either functors is in place of try… Catch, which uses an lvalue to indicate an error.

const parseJSON = jsonStr= > {
	try{
		return Right.of(JSON.parse(jsonStr))
	} catch (e) {
		return Left.of(e.message)
	}
}
Copy the code

1.8.5 Monad functor

A functor is a container that can contain any value. It is perfectly legal to have a functor within a functor. However, this creates the problem of multi-layer nested functors.

Functor.of(
  Functor.of(
    Functor.of(1)))Copy the code

There are three levels of nesting in the code above, and to retrieve the lowest value, you need to use this.val three times in a row. Hence the Monad functor.

The purpose of Monad functors is to always return a single-layer functor. It has a flatMap method, which does the same thing as the Map method, except that if a nested functor is generated, it fetches the values inside the nested functor, ensuring that it always returns a single layer container without nesting.

class Monad extands Functor {
    join() {
        return this.val
    }
    
    flatMap(fn) {
        return this.map(fn).join()
    }
}
Copy the code

If f returns a functor, this.map(f) generates a nested functor. Therefore, the Join method ensures that the flatMap method always returns a single-layer functor. This means that nested functors are flatten.

1.8.6 IO functor

Operations that are impure in IO operations need to be done by writing IO operations as Monad functors.

var fs = require('fs');

var readFile = function(filename) {
  return new IO(function() {
    return fs.readFileSync(filename, 'utf-8')}); };var print = function(x) {
  return new IO(function() {
    console.log(x)
    return x
  });
}
Copy the code

In the above code, reading a file and printing are themselves impure operations, but readFile and print are pure functions because they always return IO functors.

If the IO functor were a Monad with the flatMap method, we could call both functions as follows.

readFile('./user.txt')
	.flatMap(print)
Copy the code

The above code does the impure operation, but since flatMap still returns an IO functor, the expression is pure.

Since the return is also an IO functor, chain operations can be implemented. Therefore, in most libraries, the flatMap method is renamed chain.

var tail = function(x) {
  return new IO(function() {
    return x[x.length - 1]}); } readFile('./user.txt')
	.flatMap(tail)
	.flatMap(print)

/ / is equivalent to
readFile('./user.txt')
	.chain(tail)
	.chain(print)
Copy the code

The above code reads the file user.txt and then selects the last line to output.

Asynchronous programming

1.1 Synchronous and asynchronous

The execution environment of the Javascript language is single-threaded and only one task can be completed at a time. If there are multiple tasks, they must be queued, the first task completed, the next task executed, and so on. However, as long as one task takes a long time, subsequent tasks must wait in line, delaying the execution of the entire program. A common browser non-response (suspended animation) is usually the result of a single piece of Javascript code running for so long (such as an infinite loop) that the entire page gets stuck in one place and no other task can be performed.

To solve this problem, the Javascript language divides the execution modes of tasks into two types: synchronous and asynchronous.

Synchronization means that the latter task waits for the end of the previous task, and then executes it. The execution sequence of the program is consistent with the order of the tasks. In asynchronous mode, each task has one or more callback functions. After the end of the former task, the callback function is executed instead of the latter task, and the latter task is executed without waiting for the end of the former task. Therefore, the execution order of the program is inconsistent with the order of the tasks, and asynchronous.

1.2 Asynchronous implementation

1.2.1 Callback functions

When there are two functions f1 and f2, f1 will take a long time to execute and F2 will be executed after f1, we can pass f2 as a callback to f1:

// The callback function
function f1(callback){setTimeout(function () { callback(); },1000); }/ / execution
f1(f2)
Copy the code

In this way, we change the synchronous operation to asynchronous operation. F1 does not block the program execution, which is equivalent to executing the main logic of the program first and postponing the execution of time-consuming operation.

The advantages of the callback function are that it is simple, easy to understand, and easy to deploy, but the disadvantages are that it is easy to create a callback hell that makes code difficult to read and maintain.

1.2.2 Event Listening

This method is most commonly used when adding events to DOM elements: no DOM adds an event such as Click, which is not executed until triggered

// Event listener
const div = document.getElementById('div')
div.addEventListener('click'.function(e) {
    console.log(e)
})
Copy the code

The above functions are executed only when the click event is emitted, and multiple events can be bound, each of which can specify multiple callback functions. But too many event listeners can become too event-driven.

1.2.3 Subscribe/Publish

There is a “signal center” that “publishes” a signal when a task completes, and other tasks can “subscribe” to the signal center to know when they can start executing. This is called a publish-subscribe pattern, also known as an observer pattern.

In the VUE project, we can use EventBus to simulate the subscriber publisher pattern:

// subscribe/publish // create an empty vue instance const bus = new vue () // listen for onChange event bus.$on('onChange', Var => {console.log(val)}) $emit('onChange', 1)Copy the code

This approach is similar in nature to, but significantly superior to, event listening. Because we can monitor the program by looking at the “message center” to see how many signals exist and how many subscribers each signal has.

1. Promise

The Promise object is used to represent the final completion (or failure) of an asynchronous operation and its resulting value.

// Promise
// Create a Promise instance
const p1 = new Promise((resolve, reject) = > {
    // Asynchronous operation
    
    / / success
    resolve(res)
    
    / / failure
    reject(err)
})

p1.then(res= > {
    console.log(res)
}, err= > {
    console.log(err)
})
Copy the code

A Promise object represents a value that is not necessarily known when the Promise is created. It allows you to associate the eventual success return value or failure cause of an asynchronous operation with the appropriate handler. This allows asynchronous methods to return values as synchronous methods do: instead of returning the final value immediately, asynchronous methods return a promise to give the value to the consumer at some point in the future.

A Promise must be in one of the following states:

  • Pending: Initial state, neither honored nor rejected.
  • This is a big pity.: indicates that the operation completed successfully.
  • I have rejected the offer.: Indicates that the operation fails.

This is a big pity. The Promise object in the pending state will either be fulfilled through a value, or will be rejected through a reason (mistake). When one of these conditions occurs, our associated handlers, arrayed with the promise’s then methods, are called. If a promise has already been fulfilled or rejected by the time a corresponding handler is bound, the handler is invoked, so there is no state of contention between completing the asynchronous operation and the binding handler.

Because the promise.prototype. then and promise.prototype. catch methods return promises, they can be called chained.

We can use promise.then(), promise.catch(), and promise.finally() to associate further actions with a promise that becomes a finalised state. These methods also return a newly generated Promise object that can be used optionally to make chain calls.

const p2 = new Promise((resolve, reject) = > {
    // Asynchronous operation
    
    / / success
    resolve(res)
    
    / / failure
    reject(err)
})

// chain call
p1.then(res= > {
    console.log(res)
    return p2
}).then(res= > {
    console.log(res)
}).catch(err= > {
    console.log(err)
})
Copy the code