functional programming from Immutable.js to Redux
basic concepts
functional programming (English: functional programming), or functional programming or functional programming, is a programming paradigm. It treats computer operations as functional operations and avoids the use of program states and mutable objects. Among them, λ calculus is the most important foundation of the language. Moreover, the function of the λ calculus can accept the function as the input parameter and the output return value.
the above is Wikipedia's definition of functional programming, which can be summed up in simple words as " emphasizes the function-based software development style ".
in addition to the abstract definition, JS's functional programming has the following characteristics:
- function is a first-class citizen
- embrace Pure function , reject side effects
- use immutable values
functional programming elements
The
function is a first-class citizen
We often hear the saying, "in JS, a function is a first-class citizen". Its specific meaning is that the function has the following characteristics:
- can be passed to other functions as arguments
- can be used as the return value of another function
- can be assigned to a variable
functional first-class citizenship is a must for all functional programming languages, while another must-have feature is support for closures (the second point above actually uses closures a lot of times)
Pure function
have and only display data streams:
- input: parameter
- output: return value
if a function is pure, it should conform to the following points:
-
there can be no side effects inside the function
-
for the same input (parameter), you must get the same output.
this means that pure functions cannot rely on variables with external scope
side effects
refer to the definition of pure function "only display data flow", and the definition of side effect has "implicit data flow". Or:
- will affect the execution context and host environment outside the scope of the function, such as modifying global variables
- relies on implicit input, such as using global variables
- implicitly exchanges data with the outside world, such as network requests
immutable values
when the function parameter is a reference type, the change to the parameter will be mapped to itself.
const arr = [1, 2, 3];
const reverse = (arr) => {
arr.reverse();
};
reverse(arr);
console.log(arr); // [3,2,1]
this operation fits the definition of "side effect": external variables are modified. Destroys the display data flow of pure functions.
if you really need to design changes to the data, you should:
- copy the original data
- modify the copy result, return new data
const reverse = (arr) => {
const temp = JSON.parse(JSON.stringify(arr));
return temp.reverse();
};
arr = reverse(arr);
problems caused by copying
make read-only to external data intuitive and simple through copying, at the cost of performance .
for a large object, each modification may be just one of the attributes, so each copy will result in a large number of redundant operations. When the data scale is large and the operation frequency is high, it will bring serious performance problems.
solving copy performance issues: persisting data structures
The root of the problem with
copy mode is that only a small part of a large object changes, but you have to copy the whole object.
this situation is actually very similar to another scenario, which is Git. There are many files in a project, but I may have modified only one of them at a time. So what is my submission record this time? The processing logic is to separate the changed part from the immutable part.
The
* * Git snapshot saves the file index, not the file itself. The changed files will have new storage space + new indexes, and the unchanged files will stay in place forever. * * in the persistent data structure, it is the index of changed attributes and the index of immutable attributes
the most commonly used library for persistent data structures is Immutable.js, which is explained in detail below.
three programming paradigms in JS
JS is a multi-paradigm language, and from the development history of the front end, the mainstream framework of each period corresponds to three programming paradigms:
- JQuery: imperative programming
- React class components: object-oriented
- React Hooks, Vue3: functional programming
advantages and disadvantages of functional programming
advantages
- is good for better code organization. Because pure functions are context-independent, they naturally have the characteristics of high cohesion and low coupling
- facilitates logical reuse. The execution of pure functions is context-free, so you can better reuse
- facilitates unit testing. Pure functions must get the same output for the same input, which is convenient for automatic testing
in different scenarios.
disadvantages
- compared with imperative programming, it tends to wrap more methods, resulting in more context switching overhead.
- uses recursion more often, resulting in higher memory overhead.
- in order to implement immutable data, there will be more objects and more pressure on garbage collection.
partial function
The definition of
partial function is simply to convert a function to a function with fewer parameters, that is, to preset parameters for it.
from fn(arg1, arg2) to fn(arg1)
curry function
On the basis of partial function,
Corialization function not only reduces the number of input parameters of function, but also changes the number of times of function execution. Its meaning is to rewrite a function that accepts N input parameters to accept one input parameter and return a function that accepts the remaining 1 parameter. That is:
fn(1,2,3) => fn(1)(2)(3)
the implementation of a Corey function is also a high-frequency content of the interview, in fact, if the number of input parameters of the function is specified, then it is very easy to achieve. For example, for a function with 3 input parameters, implement the following
const curry = (fn) => (arg1) => (arg2) => (arg3) => fn(arg1, arg2, arg3);
const fn = (a, b, c) => console.log(a, b, c);
curry(fn)(1)(2)(3); // 1 2 3
then the key to implementing the universal curry function is:
- automatically determine the function input parameter
- self-recursive call
const curry = (fn) => {
const argLen = fn.length; // The number of input parameters of the original function
const recursion = (args) =>
args.length >= argLen
? fn(...args)
: (newArg) => recursion([...args, newArg]);
return recursion([]);
};
compose & pipe
compose and pipe are also common tools, and some open source libraries also have their own implementations for specific scenarios (such as Redux, koa-compose). To implement a general compose function is actually very simple, with the help of the reduce method of the array
const compose = (funcs) => {
if (funcs.length === 0) {
return (arg) => arg;
}
if (funcs.length === 1) {
return funcs[0];
}
funcs.reduce(
(pre, cur) =>
(...args) =>
pre(cur(...args))
);
};
const fn1 = (x) => x * 2;
const fn2 = (x) => x + 2;
const fn3 = (x) => x * 3;
const compute = compose([fn1, fn2, fn3]);
// compute = (...args) => fn1(fn2(fn3(...args)))
console.log(compute(1)); // 10
the difference between pipe
function and compose
is that it executes in the opposite order, just like the pipe operator in Linux, the result of the previous function flows to the input parameter of the next function, so change the reduce
method to reduceRight
:
.
const pipe = (funcs) => {
if (funcs.length === 0) {
return (arg) => arg;
}
if (funcs.length === 1) {
return funcs[0];
}
funcs.reduceRight(
(pre, cur) =>
(...args) =>
pre(cur(...args))
);
};
const compute = pipe([fn1, fn2, fn3]);
// compute = (...args) => fn3(fn2(fn1(...args)))
console.log(compute(1)); // 12
The Application of
function in Common Library
React
functional component + hook writing has become the official first push style in the latest React documents. And this is based on the concept of functional programming. The core feature of React is " data-driven view ", that is, UI = render (data)
.
The update of
UI is bound to have side effects, so how to ensure the "purity" of component functions? The answer is to manage the side effects outside the component, leaving all the side effects to hooks. The component can use state , but does not have state .
advantages of Hooks over class components:
- separation of concerns. In class components, logical code is placed in the life cycle, and the code is organized according to the life cycle. In hooks writing, the code is organized by business logic, which makes it clearer
- is easier to write. All kinds of complex design patterns based on inheritance in class component writing are omitted
Immutable.js
Immutable
is used to achieve the "immutable value" of the three elements of functional programming. My first contact was in Redux. Redux requires that state cannot be modified in reducer but should return a new state, but this is only a "normative convention", not a "code-level restriction", and Immutable is used to provide immutable data structures that do not exist in JS natively .
Immutable provides a series of custom data structures and corresponding update API, and these API will perform the update by returning the new value.
let map1 = Immutable.Map({});
map1 = map1.set("name", "youky");
console.log(map1);
Immutable internal storage reference dictionary tree (Trie)
implementation, with each modification, the immutable attribute will point to the original value with an index, and only assign a new index to the changed value. This update will be much more efficient than the overall copy.
Redux
There are also many places that embody the functional programming mode in
Redux:
- reducer if pure function (use middleware such as redux-saga if side effects are needed)
- reducer does not modify state directly, but returns a new state
- higher-order functions of middleware and Corialization
- provides a
compose
function, which is a very basic tool function in functional programming
The compose function in the
Redux source code is implemented as follows:
export default function compose(): <R>(a: R) => R;
export default function compose<F extends Function>(f: F): F;
/* two functions */
export default function compose<A, T extends any[], R>(
f1: (a: A) => R,
f2: Func<T, A>
): Func<T, R>;
/* three functions */
export default function compose<A, B, T extends any[], R>(
f1: (b: B) => R,
f2: (a: A) => B,
f3: Func<T, A>
): Func<T, R>;
/* four functions */
export default function compose<A, B, C, T extends any[], R>(
f1: (c: C) => R,
f2: (b: B) => C,
f3: (a: A) => B,
f4: Func<T, A>
): Func<T, R>;
/* rest */
export default function compose<R>(
f1: (a: any) => R,
...funcs: Function[]
): (...args: any[]) => R;
export default function compose<R>(...funcs: Function[]): (...args: any[]) => R;
export default function compose(...funcs: Function[]) {
if (funcs.length === 0) {
// infer the argument type so it is usable in inference down the line
return <T>(arg: T) => arg;
}
if (funcs.length === 1) {
return funcs[0];
}
return funcs.reduce(
(a, b) =>
(...args: any) =>
a(b(...args))
);
}
the first step is to use function overloading for type declaration.
the implementation is actually very simple:
- pass in an empty array and return a custom function that returns the received parameter
- if the length of the array passed in is 1, a unique element is returned
- assembles array elements using the reduce method and returns a new function containing nested execution of elements
Koa
in Koa's onion model, middleware functions are stored in this.middleware
by adding middleware through app.use
.
use (fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
debug('use %s', fn._name || fn.name || '-')
this.middleware.push(fn)
return this
}
combine all the middleware into a function fn through the koa-compose
module, calling
each time the request is processed.
// Callback is the processing function bound to app. list
callback () {
const fn = this.compose(this.middleware)
if (!this.listenerCount('error')) this.on('error', this.onerror)
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res)
return this.handleRequest(ctx, fn)
}
return handleRequest
}
the compose here determines the order of calls between multiple middleware. Users can pass in a custom compose function through option, or use the koa-compose
module by default. The source code is as follows:
function compose(middleware) {
if (!Array.isArray(middleware))
throw new TypeError("Middleware stack must be an array!");
for (const fn of middleware) {
if (typeof fn !== "function")
throw new TypeError("Middleware must be composed of functions!");
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
return function (context, next) {
// last called middleware #
let index = -1;
return dispatch(0);
function dispatch(i) {
if (i <= index)
return Promise.reject(new Error("next() called multiple times"));
index = i;
let fn = middleware[i];
if (i === middleware.length) fn = next;
if (!fn) return Promise.resolve();
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
};
}
also judge the parameters first. Unlike compose in redux, the middleware in koa is asynchronous and requires manual calls to the next method to transfer execution authority to the next middleware. From the code, we can see that the next parameters received in the middleware are actually dispatch.bind (null, I + 1)
, that is, the dispatch method, to achieve the purpose of recursive execution.
using bind
here actually creates a partial function. According to the definition of bind, several parameters passed after this are inserted at the top of the argument list when the function call is returned. That is to say
const next = dispatch.bind(null, i + 1))
next() // equal as dispatch(i+1)
attachment: functional programming and mathematical principles
The
function is not a proper noun in the computer field. In fact, the word function was first used by Leibniz in 1694.
The idea of
functional programming actually contains the ideas of mathematical principles such as category theory and group theory.