Functional programming from Immutable.js to Redux

April 17, 2023 1365hotness 0likes 0comments

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:

  1. can be passed to other functions as arguments
  2. can be used as the return value of another function
  3. 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:

  1. there can be no side effects inside the function

  2. 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
  • in different scenarios.

  • facilitates unit testing. Pure functions must get the same output for the same input, which is convenient for automatic testing

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.

InterServer Web Hosting and VPS

Aaron

Hello, my name is Aaron and I am a freelance front-end developer

Comments