Functional programming

Time:2021-3-22

Basic theory

  • Compared with the history of computer, functional programming is a very old concept, even before the birth of the first computer. The basic model of functional programming comes from lambda (x = > x * 2) calculus, which is not designed to be executed on computer. It is a formal system introduced in 1930s to study function definition, function application and recursion.
  • Functional programming is neither function programming nor traditional process oriented programming. The main idea is to combine complex function operators into simple functions (computational theory, or recursion theory, or ramda calculus). The operation process should be written as a series of nested function calls
  • The real heat is gradually warming up with the higher-order function of react

Category theory

  • Functional programming is a branch of category theory. It is a very complex mathematics. It is believed that all concept systems in the world can abstract out one category after another
  • There is a certain relationship between each other, concepts, things, objects and so on, all constitute categories. Anything can be defined by finding out the relationship between them
  • The arrow denotes the relationship between the members of a category. Its formal name is morphism. According to category theory, all members of the same category are transformations of different states. Through morphism, one member can change into another.

Common core concepts of functional programming

  • Pure function
  • Partial application function and coriolisation of function
  • Function combination
  • Point Free
  • Declarative and imperative code
  • Lazy evaluation

Pure function

Definition: for the same input, you will always get the same output, without any observable side effects or depending on the state of the external environment.

var xs = [1,2,3,4,5];

//  Array.slice It's a pure function because it has no side effects. For a fixed input, the output is always fixed

xs.slice(0,3); // [1,2,3]

xs.slice(0,3); // [1,2,3]

advantage:

import _ from 'lodash';

var sin = _.memorize(x => Math.sin(x));

//The first calculation will be a little slower

var a = sin(1);

//The second time I had a cache, it was very fast

var b = sin(1);

//Pure functions can not only effectively reduce the complexity of the system, but also have many great features, such as cacheability

Disadvantages:

//Impure

var min = 18;

var checkAge = age => age > min;

//Pure. It's very functional

var checkAge = age => age > 18;

//In the impure version, checkage depends not only on age, but also on the externally dependent variable min.

//The pure checkage hard codes the key number 18 in the function, which has poor expansibility. It can be solved by Coriolis elegant function.

Purity and idempotency

Idempotency means that the same effect can be obtained after executing countless times, and the same parameter running a function should be consistent with the result of two consecutive times. Idempotency is related to purity in functional programming, but it is inconsistent.


Math.abs(Math.abs(-42))

Partial application function

  • Pass some parameters to the function to call it and let it return a function to handle the remaining parameters.
  • The reason why partial function is partial is that it can only deal with those inputs that can match at least one case statement, but not all possible inputs
//With a function parameter and some parameters of the function

const partial = (f, ...args) =>

(...moreArgs) => f(...args, ...moreArgs)

const add3 = (a, b, c) => a + b + c

//Partial application of '2' and '3' to 'add3' gives you a single parameter function

const fivePlus = partial(add3, 2, 3)

console.log(fivePlus(4)) // 9

//Using bind to achieve

const add1More = add3.bind(null, 2, 3)

console.log(add1More(4)) // 9

The coritrization of functions

  • Curried is realized by partial application function.
  • Pass some parameters to the function to call it and let it return a function to handle the remaining parameters
//Before curry

function add(x, y) {

return x + y;

}

add(1, 2) // 3

//After curry

function addX(y) {

return function (x) {

return x + y;

};

}

addX(2)(1) // 3

//Bind realizes Coriolis

function foo(p1, p2) {

this.val = p1 + p2;

}

var bar = foo.bind(null, "p1");

var baz = new bar("p2");

console.log(baz.val); // p1p2

In fact, coritrization is a method of “preloading” functions. By passing fewer parameters, we can get a new function that has remembered these parameters. In a sense, it is a “cache” of parameters and a very efficient method of writing functions

Function combination

In order to solve the problem of function nesting, we need to use “function combination” to make multiple functions like building blocks


const compose = (f, g) => (x => f(g(x)));

var first = arr => arr[0];

var reverse = arr => arr.reverse();

var last = compose(first, reverse);

console.log(last([1,2,3,4,5])); // 5

Point Free

Turn some methods that come with objects into pure functions instead of naming transient intermediate variables.


const compose = (f, g) => (x => f(g(x)));

var toUpperCase = word => word.toUpperCase();

var split = x => (str => str.split(x));

var f = compose(split(' '), toUpperCase);

console.log(f("abcd aaa"));// [ 'ABCD', 'AAA' ]

This style can help us reduce unnecessary naming and keep the code concise and generic.

Declarative and imperative code

Imperative code means that we write one instruction after another to make the computer perform some actions, which usually involves a lot of complicated details. The declarative is much more elegant. We write expressions to declare what we want to do, rather than step-by-step instructions.

//Imperative

let ceoList = [];

for(var i = 0; i < companies.length; i++)

ceoList.push(companies[i].CEO)

}

//Declarative

let ceoList = companies.map(c => c.CEO);

Inert evaluation, inert function

Lazy functions are easy to understand. If the same function has a large number of ranges, and there are many judgments inside the function to detect the function, it will waste time and browser resources for a call. When the first judgment is completed, rewrite the function directly without judgment.

//Perform different functions according to the browser environment

//Common writing

function normalCreate() {

if (isChrome()) {

console.log('normal create in chrome');

} else {

console.log('normal create in other browser');

}

}

normalCreate();

normalCreate();

//Writing inert function

function lazyLoadCreate () {

console.log('first creating'); // pos-1

if (isChrome()) {

lazyLoadCreate = function () {

console.log('create function in chrome');

}

} else {

lazyLoadCreate = function () {

console.log('create function in other browser');

}

}

return lazyLoadCreate();

}

lazyLoadCreate();

lazyLoadCreate();

Functional programming – a more technical term

  • Higher order function
  • Tail call optimization PTC
  • closure
  • Container, functor
  • Error handling, either, AP
  • IO
  • Monad

Higher order function

Function as a parameter, encapsulate the incoming function, and then return the encapsulated function to achieve a higher degree of abstraction.


var add = function(a, b){

return a + b;

};

function math(func,array){

return func(array[0], array[1]);

}

math(add,[1,2]); // 3

Tail call optimization

The last action inside a function is a function call. The return value of the call is returned directly to the function.. The function call itself is called recursion. If the tail calls itself, it is called tail recursion. Recursion needs to save a large number of call records, so stack overflow error is easy to occur. If tail recursion optimization is used to turn recursion into a loop, only one call record needs to be saved, so stack overflow error will not occur.

//Normal recursion

function factorial(n) {

if (n === 1) return 1;

return n * factorial(n - 1);

}

//Tail recursion

function factorial(n, total) {

if (n === 1) return total;

return factorial(n - 1, n * total);

}

Traditional recursion

In normal recursion, memory needs to record the depth and location of the call stack. Calculate the return value at the lowest level, and then jump back to the upper level according to the recorded information, and then jump back to a higher level, and run in turn until the outermost calling function. In CPU computing and memory will consume a lot, and when the depth is too large, there will be stack overflow

function sum(n){

if (n === 1) return 1;

return n + sum(n - 1);

}

//Execution process

// sum(5)

// (5 + sum(4))

// (5 + (4 + sum(3)))

// (5 + (4 + (3 + sum(2))))

// (5 + (4 + (3 + (2 + sum(1)))))

// (5 + (4 + (3 + (2 + 1))))

// (5 + (4 + (3 + 3)))

// (5 + (4 + 6))

// (5 + 10)

// 15

Fine tail recursion

function sum(x, total) {

if (x === 1) {

return x + total;

}

return sum(x - 1, x + total);

}

//Execution process

// sum(5, 0)

// sum(4, 5)

// sum(3, 9)

// sum(2, 12)

// sum(1, 14)

// 15

The whole calculation process is linear. After calling sum (x, total) once, it will enter the next stack, and the relevant data and information will follow, and will no longer be stored on the stack. When the final value is calculated, it returns directly to the top sum (5,0). This can effectively prevent stack overflow.

be careful

  • The criterion of tail recursion is whether the function calls itself in the last step, rather than whether it calls itself in the last line of the function. The last line calls other functions and returns the tail call.
  • According to the logic, the recursive call stack always updates the current stack frame, which completely avoids the risk of stack explosion. But today’s browsers don’t fully support it. There are two reasons: 1. Eliminating recursion at the engine level is an implicit behavior that developers don’t realize. 2. The stack information is lost, which is hard for developers to debug.

closure

In the following example, although the outer makepowerfn function is executed and the call frame on the stack is released, the scope on the heap is not released, so power can still be accessed by the powerfn function, thus forming a closure


function makePowerFn(power) {

function powerFn(base) {

return Math.pow(base, power);

}

return powerFn;

}

var square = makePowerFn(2);

square(3); // 9

Category and container

  1. We can think of “category” as a container containing two things. Value, the deformation relation of value, that is function.
  2. Category theory uses functions to express the relationship between categories.
  3. With the development of category theory, a set of operation methods of functions have been developed. At first, this method was only used for mathematical operations, but later it was implemented on a computer, which became today’s “functional programming”.
  4. In essence, functional programming is only the operation method of category theory. It is the same kind of things as mathematical logic, calculus and determinant. They are all mathematical methods, but it happens that they can be used to write programs. Why does functional programming require that the function must be pure and not have side effects? Because it is a kind of mathematical operation, the original purpose is to evaluate, do nothing else, otherwise it will not be able to meet the functional algorithm.
  5. Function can be used not only to transform the median value of the same category, but also to transform one category into another. That’s about itFunctor
  6. Functor is not only the most important data type in functional programming, but also the basic unit of operation and function. First of all, it is a category, that is to say, a container, which contains the value and deformation relationship. In particular, its deformation relationship can act on each value in turn, changing the current container into another.

Functor (functor)

  1. The object returned by $(…) is not a native DOM object, but an encapsulation of the native object. In a sense, it is a “container” (but it is not functional)
  2. Functor is a container type that obeys certain rules
  3. Functor is an abstraction of function calls. We give the container the ability to call functions. When we put things into a container, we only leave an interface map for functions outside the container. When we map a function, we let the container run the function by itself. In this way, the container can freely choose when, where and how to operate the function, so that it has the characteristics of lazy evaluation, error handling, asynchronous call and so on
var Container = function(x) {

this.__value = x;

}

//The general convention of functional programming is that functors have an of method

Container.of = x => new Container(x);

//Generally, the flag of functor is that the container has map method. This method maps each value in the container to another container.

Container.prototype.map = function(f) {

return Container.of(f(this.__value))

}

Container.of(3)

. map (x = > x + 1) // result container (4)

.map(x => 'Result is ' + x); // Container('Result is 4')

ES6 writing


class Functor {

constructor(val) {

this.val = val;

}

map(f) {

return new Functor(f(this.val));

}

}

(new Functor(2)).map(function (two) {

return two + 2;

}); // Functor(4)

map

  • In the above code, functor is a functor whose map method takes function f as a parameter, and then returns a new functor containing values processed by F (f)( this.val ))
  • Generally, the flag of functor is that the container has map method. This method maps each value in the container to another container
  • The above example shows that the operations in functional programming are completed by functors, that is, the operations are not directly directed at the value, but the container of the value, functors. The functor itself has an external interface (map method), and various functions are operators, which are connected to the container through the interface, causing the deformation of the values in the container
  • Therefore, learning functional programming is actually learning various operations of functors. Since the operation method can be encapsulated in the functor, various types of functors are derived. As many operations as there are, there are as many kinds of functors. Functional programming becomes the use of different functors to solve practical problems

May functor

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


var Maybe = function(x) {

this.__value = x;

}

Maybe.of = function(x) {

return new Maybe(x);

}

Maybe.prototype.map = function(f) {

return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value));

}

Maybe.prototype.isNothing = function() {

return (this.__value === null || this.__value === undefined);

}

Either functor

Conditional operation, if… Else, is one of the most common operations. In functional programming, it is expressed by the either functor. There are two values in the either functor: left and right. The right value is normally used, and the left value is the default value when the right value does not exist

class Either extends Functor {

constructor(left, right) {

this.left = left;

this.right = right;

}

map(f) {

return this.right ?

Either.of(this.left, f(this.right)) :

Either.of(f(this.left), this.right);

}

}

Either.of = function (left, right) {

return new Either(left, right);

//Use

var addOne = function (x) {

return x + 1;

};

Either.of(5, 6).map(addOne);

// Either(5, 7);

Either.of(1, null).map(addOne);

// Either(2, null);
var Left = function(x) {

this.__value = x;

}

var Right = function(x) {

this.__value = x;

}

Left.of = function(x) {

return new Left(x);

}

Right.of = function(x) {

return new Right(x);

}

//It's different here!!!

Left.prototype.map = function(f) {

return this;

}

Right.prototype.map = function(f) {

return Right.of(f(this.__value));

}

The only difference between left and right is the implementation of the map method, Right.map The behavior of is the same as the map function we mentioned earlier. But Left.map It’s very different: it doesn’t do anything to the container, it just simply takes the container in and throws it out. This feature means that left can be used to pass an error message.


var getAge = user =>

user.age ? Right.of(user.age) : Left.of("ERROR!");

getAge({name: 'stark', age: '21'})

.map(age => 'Age is ' + age); //=> Right('Age is 21')

getAge({name: 'stark'}).map(age => 'Age is ' + age); //=> Left('ERROR!')

Left can make the error of any link in the call chain return to the end of the call chain immediately, which brings us great convenience in error handling

AP functor

The value contained in the functor may be a function. We can imagine a case where the value of one functor is a numerical value and the value of the other functor is a function.


function addTwo(x) {

return x + 2;

}

const A = Functor.of(2);

const B = Functor.of(addTwo)

In the above code, functorAThe internal value is2, functorBThe internal value is a functionaddTwo.

Sometimes we want lettersBInternal function, you can use functorAInternal values. This is where you need to use itapLetter.

AP is the abbreviation of application. Everything is deployedapThe functor of method is AP functor.

class Ap extends Functor {

ap(F) {

return Ap.of(this.val(F.val));

}

}

//The parameter of AP method is not a function, but another functor.

//The above example can be written in the following form

Ap.of(addTwo).ap(Functor.of(2))

// Ap(4)

The significance of AP functor is that for those functions with multiple parameters, it can take values from multiple containers and realize the chain operation of functor


function add(x) {

return function (y) {

return x + y;

};

}

Ap.of(add).ap(Maybe.of(2)).ap(Maybe.of(3));

// Ap(5)

Monad functor

Monad is a kind of design pattern, which means that an operation process is divided into several interconnected steps through functions. As long as you provide the functions needed for the next operation, the whole operation will be carried out automatically

The function of monad functor is to always return a single layer functor. It has oneflatMapMethods, andmapMethods have the same function, the only difference is that if a nested functor is generated, it will take out the internal value of the latter to ensure that it will always return a single-layer container without nesting.


class Monad extends Functor {

join() {

return this.val;

}

flatMap(f) {

return this.map(f).join();

}

}

In the above code, if the functionfReturn is a functor, thenthis.map(f)A nested functor is generated. So,joinThe method ensures thatflatMapMethod always returns a single level functor. This means that nested functors are flattened

IO operation

The real program has to touch the dirty world

The important application of monad functor is to realize I / O operation.

I / O is not pure operation, ordinary functional programming can not do, then you need to write IO operation asMonadFunctor, through it to complete.


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;

});

}

In the above code, reading the file and printing itself are impure operations, butreadFileandprintThey are pure functions because they always return IO functors.

If the IO functor is aMonad, withflatMapMethod, then we can call these two functions as follows.


readFile('./user.txt')

.flatMap(print)

This is the magic place, the above code completed the impure operation, but becauseflatMapIt returns an IO functor, so the expression is pure. We use a pure expression to complete the operation with side effects, which is what monad does.

Because the return is still an IO functor, chain operation can be realized. So, in most libraries,flatMapThe method was renamedchain

var tail = function(x) {

return new IO(function() {

return x[x.length - 1];

});

}

readFile('./user.txt')

.flatMap(tail)

.flatMap(print)

//Equivalent to

readFile('./user.txt')

.chain(tail)

.chain(print)

The above code reads the file user.txt , and then select the last line of output

summary

Functional programming should not be seen as a panacea. Instead, it should be seen as a natural complement to our existing Toolbox – it brings higher composability, flexibility, and fault tolerance. Modern JavaScript libraries have begun to embrace the concept of functional programming to gain these advantages. Redux, as a variant of flux, is based on state machine and functional programming.

In terms of software engineering, “there is no silver bullet”. Functional programming is not omnipotent either. It is just a programming paradigm just like OOP in rotten street. In many practical applications, it is difficult to express them in functional form. It may be easier to choose OOP or other programming paradigms. But we should pay attention to the core idea of functional programming. If OOP reduces complexity by good encapsulation, inheritance, polymorphism and interface definition, then functional programming reduces system complexity by pure functions and their combination, curry, functor and other technologies, while react, rxjs, rxjs, etc Cycle.js It is the endorsement of this idea. Let’s embrace functional programming and open the door of your program!

Reference Ruan Yifeng – functional programming
Permanent address functional programming