Functional programming
Functional programming is a parallel programming paradigm with object-oriented programming and Procedural programming.
Category theory
- Category theory is the origin of functional programming.
- Category Theory is an abstract branch of mathematics, which formalizes some concepts in such branches as Set Theory, type Theory, group Theory, and Logic.
- A category is a collection of objects and their relations, and the relations between these objects (called morphisms, morphisms) are well behaved in terms of composition and associativity. The points in the figure above, with the arrows between them, form a category. All the members (objects) of the same category are “transformations” of different states. The arrows represent the relationships among the members of the category, formally known as “morphism.”
- The set category consists of sets and the relationships (maps) between them; The category of groups consists of groups and their relations (group homomorphisms); Topological space categories consist of topological Spaces and their relationships (continuous mappings). In functional programming, members (objects) are data types, for example
String
,Boolean
,Number
和Object
And so on; Morphisms are standard, ordinary pure functions. - Functional programming is essentially a category theory of operations, the same kind of thing as mathematical logic, calculus, and determinants, all mathematical methods. It’s a mathematical operation, and it’s theoretically possible to compute from one member of the category, the other members from a function, so functional programming requires functions to be pure functions.
Pure functions
A pure function is one in which the same input always returns the same output without any observable side effects.
/ / not pure
let minimum = 21
let checkAge = function(age) {
return age >= minimum
}
/ / pure
let checkAge = function(age) {
let minimum = 21
return age >= minimum
}
Copy the code
Side effects
A side effect is a change in the state of the system, or an observable interaction with the outside world, during the calculation of the result.
Side effects may include, but are not limited to:
- Changing a file system
- Insert records into the database
- Send a
http
request - Variable data
- Print/log
- Get user input
- DOM query
- Accessing system Status
In general, any interaction with the function’s external environment is a side effect.
Function in mathematics
A pure function is a function in mathematics:
- A function is a relationship between two values: input and output.
- Each input value returns and only one output value, a one-to-one mapping.
- Different inputs can have the same output.
The benefits of pure functions
The cache can be
Pure functions can always be cached based on input, and a typical way to implement caching is memoize technology.
const memoize = function(f) {
const cache = {}
return function(. args) {
const arg_str = JSON.stringify(args)
cache[arg_str] = cache[arg_str] || f.apply(f, args)
return cache[arg_str]
}
}
const squareNumber = memoize(x= > x * x)
squareNumber(4); / / = > 16
squareNumber(4); // => 16 Reads the result of input value 4 from the cache
Copy the code
Removable value
The dependency of a pure function is clear. All dependencies of a pure function must be passed by parameters. From the function signature we can infer the function’s function.
Pure functions are context-independent, can run anywhere, and portability can mean serializing the function and sending it over the socket. It can also mean that the code can run in Web Workers.
testability
Pure functions make testing easier. We don’t need to fake a “real” environment or assert the state before and after every test. Simply give the function an input and assert the output.
Curry
Curry and compose are the two most basic operations of functional programming.
Partial Applications/Partial Applications
A partial function is a function that fixes one or more arguments to a function and then produces another function with smaller elements, that is, it converts an n-element function into an n-x element function.
// Factory for generating partial functions
const partial = (f, ... args) = > (. moreArgs) = >f(... args, ... moreArgs)const rightPartial = (f, ... args) = > (. moreArgs) = >f(... moreArgs, ... args)const add = (a, b, c) = > a + b + c
// Returns a single-argument function with fixed arguments 1 and 2
const fiveThree = partial(add, 1.2) // Add. Bind (null, 1, 2)
fiveThree(3) / / = > 6
Copy the code
Currie,
Currie transformation is to convert a multi-parameter function into multiple nested single-parameter functions, that is, to convert an n-yuan function into n nested unary functions. So I’m going to convert f of a, b, c to f of a, b, c.
const add = (a, b, c) = > a + b + c
// After currization
// const curryAdd = a => b => c => a + b + c
const curryAdd = function(a) {
return function(b) {
return function(c) {
return a + b + c
}
}
}
add(1.2.3) === curryAdd(1) (2) (3) = = =6
Copy the code
// The Curry function also allows multiple arguments to be passed at once
const curry = fn= > {
return judge = (. args) = >args.length === fn.length ? fn(... args) :(. moreArgs) = >judge(... args, ... moreArgs) }const add = (a, b, c) = > a + b + c
const curryAdd = curry(add)
add(1.2.3) / / = > 6
curryAdd(1.2.3) / / = > 6
curryAdd(1.2) (3) / / = > 6
curryAdd(1) (2.3) / / = > 6
curryAdd(1) (2) (3) / / = > 6
Copy the code
The curry
- Function corrification, is a fixed part of the argument, return a function that accepts the rest of the argument, also known as the partial calculation function, in order to narrow the scope of application, to create a more targeted function.
- The inverse Currified function, literally, means the exact opposite of the currified function, extending the scope, creating a function with a wider scope. Make a method that is only suitable for a particular object extend to more objects.
const unCurring = function(fn) {
return function(. args) {
const obj = args.shift()
return fn.apply(obj, args)
}
}
const push = unCurring(Array.prototype.push)
// Objects can also be pushed
const obj = {}
push(obj, 'one'.'two') // => {0: 'one', 1: 'two', length: 2}
Copy the code
Compose
If a value has to pass through multiple functions to become another value, the solution to function nesting (onion code h(g(f(x))) is to combine all intermediate steps into a single function, which is called compose.
const compose = (f, g) = > (x= > f(g(x)))
// first:: [a] -> a
const first = arr= > arr[0]
// reverse:: [a] -> [a]
const reverse = arr= > arr.reverse()
const last = compose(first, reverse)
last([1.2.3.4]) / / = > 4
Copy the code
// The generic compose function
const compose = (. funcs) = > {
const identity = x= > x
return funcs.reduce((a, b) = > (. args) = >a(b(... args)), identity) }Copy the code
Compose function features:
- The argument is multiple functions and the return value is a “combinatorial function”;
- All functions in the combination function are executed from right to left, and the execution from right to left can reflect the mathematical meaning more.
- Except for the first function whose arguments are multiple, the arguments of the other functions all receive the return value of the previous function. In general, the convention is that every function is unary.
Associative law:
/** * The following three methods have the same effect */
compose(f, compose(g, h))
compose(compose(f, g), h)
compose(f, g, h)
Copy the code
Pipe
- The compose function streams data from right to left and the PIPE function streams data from left to right (gulp, RXJS).
// RXJS is the mainstream reactive programming library based on functional programming
import { from } from 'rxjs'
import { map, filter, catchError } from 'rxjs/operators'
from([1.2.3]).pipe(
filter(x= > x % 2= = =0), // Filter out even numbers
map(x= > x * x), / / square
catchError(err= > console.error(err)) // Error handling
).subscribe(console.log)
Copy the code
pipe(... operations: OperatorFunction<any, any>[]): Observable<any> {if (operations.length === 0) {
return this as any;
}
if (operations.length == 1) {
return operation[0];
}
return operations.reduce((prev, fn) = > fn(prev), this);
}
Copy the code
Functional programming has three main characteristics: declarative, immutable, and no side effects. A functional program is a program that pipes data between a series of pure functions.
Category and container
We can think of a category as a container containing two things: value and deformation relation of value (function).
const Container = function(x) {
this.__value = x
}
The // of function is used to generate new containers
Container.of = function(x) {
return new Container(x)
}
Copy the code
Functions can be used not only to convert values between categories, but also to convert one category into another. This is where functors come in.
A functor that implements the of method is called a conservative functor.
Functor
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.
- The sign of a functor is that the container has
map
Methods. This method maps every value in a container to another container.
class Functor {
constructor(x) {
this.__value = x
}
map(f: Function): Functor {
return Functor.of(f(this.__value))
}
}
Functor.of = function(x) {
return new Functor(x)
}
Copy the code
- In the code above,
Functor
It’s a functor. It’s a functormap
The function f () method takes the function f as an argument and returns a new functor containing the values treated by f (f(this.val)
). - 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
The various functions are operators that interface into the container and cause the deformation of the value inside the container.Container
Is passed tomap
After the function, we can do whatever we want; At the end of the operation, to prevent accidents to put it back where it belongsContainer
. The result of this is that we can call continuouslymap
Run whatever function we want to run. Has been calledmap
, is the combination (compose
).
Maybe functor
Functors accept various functions through a map and process values inside the container. 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, which is likely to cause an error.
Functor.of(null).map(x= > x.toString()) // => TypeError
Copy the code
The Maybe functor is used to handle this. Maybe’s map function checks for null values first and then calls the function passed in so it can handle null values without error.
class Maybe extends Functor {
isNullOrUndefined() {
return this.__value === null || this.__value === undefined
}
map(f) {
return this.isNullOrUndefined() ? Maybe.of(null) : Maybe.of(f(this.__value))
}
}
Maybe.of = function(x) {
return new Maybe(x)
}
Maybe.of(null).map(x= > x.toString()) // => Maybe(null)
Copy the code
Either functor
Either functors are a purer way of handling errors. They have 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.
// Left
class Left extends Functor {
map(f) {
return this
}
}
Left.of = function(x) {
return new Left(x);
}
// Right
class Right extends Functor {
map(f) {
return new Right.of(f(this.__value))
}
}
Right.of = function(x) {
return new Right(x)
}
const 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
The ability to make any error in the chain immediately go back to the end of the chain gives us a great convenience in error handling, instead of having to have layer upon layer of errorstry/catch
.
IO functor
A function that values from localStorage has side effects, but wrapping it in another function and delaying execution makes it look like a pure function.
const getFromStorage = function(key) {
return function() {
return localStorage[key]
}
}
Copy the code
The __value of an IO functor is a function that wraps impure operations (e.g., IO, network requests) in a function to delay the execution of the impure action.
class IO {
constructor(f: Function) {
this.__value = f
}
map(f) {
return IO.of(compose(f, this.__value))
}
}
IO.of = function(x) {
return new IO(function() {
return x
})
}
Copy the code
Application functor (AP)
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
We want functions inside functor B to be able to operate on values inside functor A. That’s where the AP functor comes in.
Ap functor is a kind of conservative functor that implements AP method and entrusts different functors with the ability to apply to each other.
class Ap extends Functor {
ap(F: Functor) {
return Ap.of(this.__value(F.__value))
}
}
Ap.of = function(x) {
return new Ap(x)
}
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(Functor(2)).ap(Functor(3)) // => Ap(5)
Copy the code
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 would result in multiple layers of nested functors.
Functor.of(Functor.of(Functor.of(6)))
Copy the code
The above functor fetches the internal value three times in a row. You need monad functors at this point.
Monad is a conservative functor that can flatten.
class Monad extends Functor {
join() {
return this.__value
}
flatMap(f) {
return this.map(f).join()
}
}
Copy the code
The purpose of Monad functors is to always return a single layer functor. It has a flatMap method, which has the same function as the Map method. The only difference is that if a nested functor is generated, the flatMap will extract the value inside the nested functor, ensuring that it will always return a single-layer container without nesting, that is, the nested functor will be flatten.
application
Rxjs
(Functional responsive programming)lodash
、underscore
redux
(State machines and functional programming)
Store -> Container state -> __value action -> Functor mapmiddleware -> IO FunctorCopy the code
conclusion
- Functional programming is not a panacea
OOP
Same, just a programming paradigm.OOP
Reducing complexity depends on good encapsulation, inheritance, polymorphism, and interface definition. Functional programming is done through pure functions and their combinations, currization,Functor
And other technologies to reduce system complexity. - Many practical applications are difficult to express with functional programming, just keep in mind that the dependence of functions on external states is the main cause of greatly increased system complexity, and keep functions as pure as possible.
The resources
- Functional programming introductory tutorial
- Functional programming is north
- Functional programming terms