The basic theory
Permanent text address functional programming looks forward to your star
- Functional Programming is actually a very old concept relative to the history of computers, even before the birth of the first computer. The basic model of functional programming is derived from the Lambda (Lambda x=>x*2) calculus, which was not designed to be executed on a computer. It was introduced in the 1930s as a formal system for studying function definitions, function applications, and recursion.
- Functional programming is not programming with functions, nor is it traditional procedural programming. The idea is to fit complex functions into simple ones (theory of computation, or recursion, or Ramda calculus). Try to write the operation as a series of nested function calls
- The real heat comes from the higher order function of React
Category Theory
- Functional programming is the mathematical branch of category theory which is a very complex mathematics, the idea that every conceptual system in the world can be abstracted into a category
- Concepts, things, objects, and so on, all constitute categories. Anything can be defined by finding a relationship between them
- The arrows, which represent the relationships between the members of a category, are formally known as morphism. Category theory holds that all members of the same category are “transformations” of different states. Through “morphism”, one member can morph into another.
Common core concepts of functional programming
- Pure functions
- Partial application of functions, currization of functions
- Function composition
- Point Free
- Declarative versus imperative code
- Inertia is evaluated
Pure functions
Definition: For the same input, always get the same output, without any observable side effects, and independent of the state of the external environment.
var xs = [1.2.3.4.5];
// array. slice is a pure function because it has no side effects. For fixed inputs, the output is always fixed
xs.slice(0.3); / / [1, 2, 3]
xs.slice(0.3); / / [1, 2, 3]
Copy the code
Advantages:
import _ from 'lodash';
var sin = _.memorize(x= > Math.sin(x));
// It will be a little slower the first time
var a = sin(1);
// The second time there is a cache, very fast
var b = sin(1);
// Pure functions not only reduce system complexity, but also have many nice features, such as cacheability
Copy the code
Disadvantages:
/ / not pure
var min = 18;
var checkAge = age= > age > min;
// Pure, this is very functional
var checkAge = age= > age > 18;
// In impure versions, checkAge depends not only on age but also on the external dependent variable min.
// The key number 18 is hardcoded in the checkAge function.
Copy the code
Purity and idempotency
Idempotence means that the same effect can be achieved after being executed countless times. A function run once with the same parameters should have the same result twice in succession. Idempotence is related to purity in functional programming, but is inconsistent.
Math.abs(Math.abs(-42))
Copy the code
Partial application function
- Call a function by passing it a set of arguments and have it return a function to process the rest of the arguments.
- Partial functions are biased in that they can only process input that matches at least one case statement, not all possible inputs
// Take a function argument and part of the function argument
const partial = (f, ... args) = >
(. moreArgs) = >f(... args, ... moreArgs)const add3 = (a, b, c) = > a + b + c
// Apply '2' and '3' to 'add3' to give you a single-argument function
const fivePlus = partial(add3, 2.3)
console.log(fivePlus(4)) / / 9
// Bind
const add1More = add3.bind(null.2.3)
console.log(add1More(4)) / / 9
Copy the code
Currization of a function
- Curried is implemented by partial application of functions.
- Call a function by passing it a set of arguments and have it return a function to process the rest of the arguments
// Before currization
function add(x, y) {
return x + y;
}
add(1.2) / / 3
// After currization
function addX(y) {
return function (x) {
return x + y;
};
}
addX(2) (1) / / 3
// bind implements currization
function foo(p1, p2) {
this.val = p1 + p2;
}
var bar = foo.bind(null."p1");
var baz = new bar("p2");
console.log(baz.val); // p1p2
Copy the code
In fact, currification is a method of “preloading” functions by passing fewer parameters to a new function that has already remembered those parameters. In a sense, it is a “cache” of parameters, which is a very efficient way to write functions
Function composition
To solve the problem of nested functions, we need to use “function composition”, where multiple functions are 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
Copy the code
Point Free
Turn some of your object’s native methods into pure functions, and don’t name 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' ]
Copy the code
This style helps reduce unnecessary naming and keeps the code simple and generic.
Declarative versus imperative code
Imperative code means that we write instruction after instruction to make a computer do something, usually involving a lot of detail. Declarative is much more elegant, we declare what we want to do by writing expressions rather than by giving step-by-step instructions.
/ / imperative
let ceoList = [];
for(var i = 0; i < companies.length; i++)
ceoList.push(companies[i].CEO)
}
/ / the declarative
let ceoList = companies.map(c= > c.CEO);
Copy the code
Lazy evaluation, lazy function
Lazy functions are easy to understand. If the same function has a lot of scope, and the function has a lot of judgment inside it to check the function, this will waste time and browser resources on a call, so when the first judgment is done, just rewrite the function, no longer need to judge.
// Perform different functions depending on the browser environment
//
function normalCreate() {
if (isChrome()) {
console.log('normal create in chrome');
} else {
console.log('normal create in other browser');
}
}
normalCreate();
normalCreate();
// inert function writing
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();
Copy the code
Functional programming – a more technical term
- Higher-order functions
- The tail call optimizes PTC
- closure
- Containers, Functor
- Error handling, Either, AP
- IO
- Monad
Higher-order functions
Function as an argument, the function passed as a wrapper, and then return the wrapper 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
Copy the code
Tail-call optimization
The last action inside a function is a function call. The return value of this call is returned directly to the function. The function calls itself, called recursion. If the tail calls itself, it is called tail recursion. Recursion requires saving a large number of call records and is prone to stack overflow errors. If you use tail recursion optimization to turn recursion into a loop, you only need to save one call record and no stack overflow errors will 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);
}
Copy the code
The traditional recursive
In normal recursion, memory needs to record the depth and position of the stack that is called. The return value is computed at the lowest level, and then, based on the recorded information, it jumps back up to the next level and runs until the outermost function is called. It consumes a lot of CPU computation and memory, and a stack overflow occurs when the depth is too deep
function sum(n){
if (n === 1) return 1;
return n + sum(n - 1);
}
// Execute 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))
// (4 + 6)
/ / (5 + 10)
/ / 15
Copy the code
Count tail recursion
function sum(x, total) {
if (x === 1) {
return x + total;
}
return sum(x - 1, x + total);
}
// Execute process
// sum(5, 0)
// sum(4, 5)
// sum(3, 9)
// sum(2, 12)
// sum(1, 14)
/ / 15
Copy the code
The calculation process is linear, sum(x, total) is called once, the next stack, the relevant data and information is entered, no longer stored on the stack. When the final value is computed, sum(5,0) is returned directly to the top level. This effectively prevents stack overflow.
Pay attention to
- Tail recursion is judged by whether the function calls itself in the last step, not whether it calls itself in the last line of the function, which calls another function and returns the last call.
- The last recursive call always updates the current stack frame, which completely avoids the danger of stack bursting. 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 the developer is not aware of. 2 Stack information lost developers have difficulty debugging.
closure
In the following example, although the outer makePowerFn function completes 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
Copy the code
Category and container
-
We can think of the category as a container containing two things. Value and the deformation relation of value, that is, function.
-
Category theory uses functions to express the relationships between categories.
-
With the development of category theory, a whole set of operation methods of functions have been developed. This method was originally used for mathematical operations, but later someone implemented it on a computer and it became functional programming today.
-
At its core, functional programming is just categorical operations, the same kind of thing as mathematical logic, calculus, and determinants, mathematical methods that happen to be used to write programs. Why does functional programming require functions to be pure and free of side effects? Because it’s a mathematical operation, and its original purpose is to evaluate, to do nothing else, otherwise it wouldn’t satisfy the functional algorithm.
-
Functions can be used not only to convert values within the same category, but also to convert one category into another. This is where functors come 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.
Functor (Functor)
-
$(…). The object returned is not a native DOM object, but rather a wrapper around the native object, which is in a sense a “container” (but not functional)
-
Functor A type of container that complies with certain rules
-
Functor is an abstraction of a function call; we give the container the ability to call the function itself. Put the things into a container, only to set aside an interface map to function outside the container, the map function, we let the container to run the function, so that the container can freely choose when and where to how to operate this function, so that the evaluated with inert, error handling, the characteristics of asynchronous invocation, and so on very cow bye
var Container = function(x) {
this.__value = x;
}
// As a general convention of functional programming, functors have an of method
Container.of = x= > new Container(x);
// As a general convention, the mark of a functor is that the container has a map method. This method maps every value in a container to another container.
Container.prototype.map = function(f) {
return Container.of(f(this.__value))
}
Container.of(3)
.map(x= > x + 1) / / the result Container (4)
.map(x= > 'Result is ' + x); // Container('Result is 4')
Copy the code
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)
Copy the code
map
- Functor is a Functor whose map method takes f as an argument and returns a new Functor containing the value treated by f(f(this.val)).
- 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
- 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 various functions are operators that access the container and cause the deformation of the values inside the container
- Therefore, learning functional programming is really learning the various operations of functors. Since operation methods can be encapsulated in functors, various kinds of functors are derived, and there are as many kinds of functors as there are operations. Functional programming becomes using different functors to solve real problems
Maybe 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
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);
}
Copy the code
Either functor
The conditional operation if… Else is one of the most common operations, expressed in functional programming using Either functors. The Either functor has two internal values: Left and Right. An rvalue is the value used normally, and an lvalue is the default value used when an rvalue 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);
Copy the code
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);
}
// This is different!!
Left.prototype.map = function(f) {
return this;
}
Right.prototype.map = function(f) {
return Right.of(f(this.__value));
}
Copy the code
The only difference between Left and Right is the implementation of the map method. Right.map behaves like the map function we mentioned earlier. But left.map is very different: it doesn’t do anything to the container, it 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! ')
Copy the code
Left allows any error in the call chain to be immediately returned to the end of the chain, which makes error handling very convenient.
AP functor
Functors contain values that could be functions. We can imagine a situation where the value of one functor is a number 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)
Copy the code
In the above code, the value inside functor A is 2, and the value inside functor B is addTwo. Sometimes we want A function inside of functor B to be able to operate on the values inside of functor A. That’s where the AP functor comes in.
Ap stands for applicative. A functor that has an AP method deployed is an AP functor.
class Ap extends Functor {
ap(F) {
return Ap.of(this.val(F.val)); }}// The argument to the 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)
Copy the code
The point of ap functors is that functions with multiple arguments can be evaluated from multiple containers to achieve chain operation of functors
function add(x) {
return function (y) {
return x + y;
};
}
Ap.of(add).ap(Maybe.of(2)).ap(Maybe.of(3));
// Ap(5)
Copy the code
Monad functor
Monad is a design pattern that represents the decomposition of an operational process into interconnected steps through functions. You just supply the function you need for the next operation, and the whole operation will go on automatically
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 extends Functor {
join() {
return this.val;
}
flatMap(f) {
return this.map(f).join(); }}Copy the code
In the above 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 will be flatten.
IO operations
The real program always touches the dirty world
An important application of Monad functors is to implement I/O (input/output) operations.
I/O is not pure operation, ordinary functional programming can not do, then need to write IO operation Monad functor, 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;
});
}
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
That’s the magic. The above code does the impure thing, but since the flatMap returns an IO functor, the expression is pure. We do the side effects with a pure expression, and that’s what Monad does.
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
conclusion
Functional programming should not be seen as a panacea. Rather, it should be seen as a natural addition to our existing toolbox — one that leads to greater composability, flexibility, and fault tolerance. Modern JavaScript libraries have begun to embrace the concept of functional programming to gain these advantages. Redux is implemented as a variation of FLUX, with state machines and functional programming at its core.
There is “no silver bullet” in software engineering, and functional programming is not a panacea, just a programming paradigm like OOP. Many practical applications are difficult to express in a functional way, and it may be easier to choose OOP or other programming paradigms. But it is important to note the core concept of functional programming. If OOP reduces complexity by good encapsulation, inheritance, polymorphism and interface definition, functional programming reduces system complexity by pure functions and their combination, currization, Functor and other techniques. React, Rxjs and Cycle.js are the spokesmen of this concept. Let’s embrace functional programming and open the door to your programs!
See Ruan Yifeng – functional programming
Reference: @ yuanzhijia