Functional programming
Why learn functional programming
An interesting survey shows that the average amount of code for programmers around the world is 5 lines / person day. In other words, you spend 7.5 hours a day reading the code (mainly studying where and how to write the five letter code), and then spend 30 minutes writing the five lines of code. This is the programmer's work of the day.
The code we write will be handed over to others or maintained by ourselves in the future, so how to improve the readability of the code is a very important task. Functional programming can do this well, so learning functional programming is very important: improve code readability. Of course, there are other factors:
- With the popularity of react, functional programming has attracted more and more attention
- vue3 also embraces functional programming
- Functional programming can abandon this
- Convenient for testing and parallel processing
- There are many libraries that can help us with functional development
What is function programming
- Baidu definition: simply put, "functional programming" is a "programming paradigm", that is, the methodology of how to write programs.
- Functional programming (FP) is to help pure functions realize fine-grained functions, and then combine fine-grained functions.
Function VS program
- The function in functional programming is not a function in function (method), but a function in Mathematics (mapping relationship, y = f(x));
- A program is an arbitrary set of functions. It may or may not have many input values. It may or may not have an output value (return value).
- The function receives the input value and explicitly return s the value
Programming paradigm
What is the programming paradigm
A pattern used in the process of solving problems in programming, which is reflected in the way of thinking and code style.
Several common programming paradigms
- Imperative programming
- object-oriented programming
- Functional programming
Different programming paradigms have different code expressions
- Imperative programming
const name = 'world'; console.log(`hello ${name}`);
- Object oriented programming: abstract the whole program into an object in real life, which will contain properties and methods. Through the concept of class, we have a factory for generating objects. Create an object using the new keyword, and finally call the object.
const Program = function(name) { this.name = name; this.greet = function() { console.log(`hello ${this.name}`); }; }; const program = new Program('world'); program.greet();
- Functional programming: complete the function of the program through the call of functions
const greet = function(name) { return `hello ${name}` }; console.log(greet('world'));
Different programming paradigms apply to different scenarios
- Imperative programming: process sequence, operation instructions on the machine
- Object oriented programming: objects, games, character models
- Functional language: data processing, business front end
Misunderstanding of functional programming
There will be a misunderstanding when you just come into contact with functional programming: only 100% of a program uses functional programming can it be called functional programming. Nothing is absolute. We usually use the combination of imperative programming, object-oriented programming, functional programming and other programming paradigms in the project process.
Function is a first-class citizen
Why functional first-class citizens
- <Programming Language Pragmatics>
In general, a value in a programming language is said to have first-class status if it can be passed as a parameter, returned from a subroutine, or assigned into a variable.
In other words, in the programming language, first-class citizens can be used as function parameters, function return values, and variables.
-
For example, strings are first-class citizens in almost all programming languages. Strings can be used as function parameters, strings can be used as function return values, and strings can also be assigned to variables.
-
For various programming languages, functions are not necessarily first-class citizens.
-
In js, a function is an ordinary object (through new Function()), we can store the function in variables and arrays, and it can also be used as the parameter and return value of another function. We can even construct a new function through new Function('alert(1) 'when the program is running.
ps: the reason why we say functional first-class citizens is that in js language, functions meet the characteristics of first-class citizens:
- Functions can be stored in variables
- Functions can be used as arguments
- Function can be used as a return value
Assign a function to a variable
let fn = function(){ console.log('hello world'); } fn();
Function as parameter
[1, 2, 3].map( item => item ** 2);
Function as return value
function getGreep() { const greepPre = 'hello '; return function(name) { console.log(`${greepPre}${name}`) } } let greep = getGreep(); greep('world');
Why learn functional first-class citizens
Functional first-class citizens are the basis for us to learn higher-order functions, Coriolis, etc.
Higher order function
What is a higher order function
Function is acceptable and returns any type of value. If a function can accept or return one or more functions, it is called a higher-order function.
That is, a function can be called a high-order function if it can meet any of the following characteristics:
- You can accept a function as an argument
- You can take a function as a return value
Function as parameter
function forEach(arr, fn) { for(let i = 0; i < arr.length; i++){ fn(arr[i]); } } forEach([1,2,3], item => console.log(item + 1));
Function as return value
function getGreep() { const greepPre = 'hello '; return function(name) { console.log(`${greepPre}${name}`) } } let greep = getGreep(); greep('world');
Significance of using higher-order functions
- Help us shield the details, just focus on the target
- Abstract general problems
Common higher order functions
forEach,map,ilter,every,some,find,reduce,sort
closure
What is a closure
When the scope of another function exists inside a function, operate on the current function. When an internal function references a variable from an external function, this is called a closure.
In fact, closure is that it can record and access variables outside its scope, even when the function is executed in different scopes
function getGreep() { const greepPre = 'hello '; return function(name) { console.log(`${greepPre}${name}`) } } let greep = getGreep(); greep('world');
The essence of closure
When a function is executed, it will be placed on an execution stack. After the function is executed, it will be removed from the execution stack. However, the scope members on the heap cannot be released because they are externally referenced. Therefore, internal functions can still access the members of external functions.
// Generate a function that calculates the power of a number function makePower (power) { return function (x) { return Math.pow(x, power); } } let power2 = makePower(2); let power3 = makePower(3); console.log(power2(4)); console.log(power3(4));
Pure function
What is a pure function
The same input will always get the same output without any observable side effects.
Slice and slice of array are pure function and impure function respectively
- slice returns the specified part of the array without changing the original array
- splice operates on the array and returns the array, which will change the original array
let numbers = [1, 2, 3, 4, 5] // Pure function numbers.slice(0, 3) // => [1, 2, 3] numbers.slice(0, 3) // => [1, 2, 3] numbers.slice(0, 3) // => [1, 2, 3] // Impure function numbers.splice(0, 3) // => [1, 2, 3] numbers.splice(0, 3) // => [4, 5] numbers.splice(0, 3) // => []
Benefits of pure functions
- Cacheable: because pure functions always have the same results for the same input, the results of pure functions can be cached
const _ = require('lodash') function getArea (r) { return Math.PI * r * r } let getAreaWithMemory = _.memoize(getArea) console.log(getAreaWithMemory(4))
- Testability: pure functions make testing easier
- parallel processing
- In a multithreaded environment, parallel operation of shared memory data is likely to occur unexpectedly
- Pure functions do not need to access shared memory data, so pure functions (web workers) can be run arbitrarily in a parallel environment
side effect
// Impure let mini = 18 function checkAge (age) { return age >= mini } // Pure (hard coded, which can be solved by coritization later) function checkAge (age) { let mini = 18 return age >= mini }
Side effects make a function impure (as in the above example). Pure functions return the same output according to the same input. If the function depends on the external state, the same output cannot be guaranteed, which will bring side effects.
Sources of side effects:
- configuration file
- database
- Get user input
All external interactions may bring side effects. The side effects also reduce the universality of the method and are not suitable for expansion and reusability. At the same time, the side effects will bring security risks to the program and uncertainty to the program, but the side effects cannot be completely prohibited and controlled within the controllable range as far as possible.
currying
How to carry out coritization
When a function has multiple parameters, first pass some parameters to call it (these parameters will never change in the future), and then return a new function to receive the remaining parameters and return the results.
function checkAge (age) { let min = 18 return age >= min } // Ordinary pure function function checkAge (min, age) { return age >= min } checkAge(18, 24) checkAge(18, 20) checkAge(20, 30) // currying function checkAge (min) { return function (age) { return age >= min } } // ES6 writing method let checkAge = min => (age => age >= min) let checkAge18 = checkAge(18) let checkAge20 = checkAge(20) checkAge18(24) checkAge18(20)
Coriolis function
// Ordinary pure function function checkAge (min, age) { return age >= min } function curry (func) { return function curriedFn (...args) { // Determine the number of arguments and formal parameters if (args.length < func.length) { return function () { return curriedFn(...args.concat(Array.from(arguments))) } } // If the number of arguments and formal parameters is the same, call func to return the result return func(...args) } } let checkAge18 = curry(checkAge)(18) let checkAge20 = curry(checkAge)(20) checkAge18(24) checkAge18(20)
Coriolis function in lodash curry(func)
- Function: create a function that receives one or more func parameters. If all the parameters required by func are provided, execute func and return the execution result. Otherwise, continue to return the function and wait to receive the remaining parameters.
- Parameter: function to be coriolised
- Return value: coriolised function
const _ = require('lodash') // Functions to be coriolised function getSum (a, b, c) { return a + b + c } // Coriolised function let curried = _.curry(getSum) // test curried(1, 2, 3) curried(1)(2)(3) curried(1, 2)(3)
summary
- Coriolism allows us to pass fewer parameters to a function to get a new function that has remembered some fixed parameters
- This is a 'cache' of function parameters
- Make the function more flexible and make the granularity of the function smaller
- Multivariate functions can be converted into univariate functions, and functions can be combined to produce powerful functions
Function combination
problem
- Pure functions and Coriolis make it easy to write onion code h(g(f(x)))
- Get the last element of the array and convert it to uppercase letters toUpper(_.first(_.reverse(array)))
What is a function combination
- If a function needs to be processed by multiple functions to get the final value, the functions of the intermediate process can be combined into one function at this time,
fn = compose(f1, f2, f3) b = fn(a
- Function combinations are executed from right to left by default
// Multi function combination function compose (...fns) { return function (value) { return fns.reverse().reduce(function (acc, fn) { return fn(acc) }, value) } } const _ = require('lodash') const toUpper = s => s.toUpperCase() const reverse = arr => arr.reverse() const first = arr => arr[0] const f = compose(toUpper, first, reverse) console.log(f(['one', 'two', 'three']))
Combinatorial functions in lodash
In lodash, the combination function flow() or flowRight() can combine multiple functions
- flow() runs from left to right
- flowRight() runs from right to left and uses more
const _ = require('lodash') const toUpper = s => s.toUpperCase() const reverse = arr => arr.reverse() const first = arr => arr[0] const f = _.flowRight(toUpper, first, reverse) console.log(f(['one', 'two', 'three']))
Characteristics of function combination
The combination of functions satisfies the combination law (the combination law in Mathematics): in compose(f, g, h), we can combine g and h, or f and G, and the results are the same.
// Associative law let f = compose(f, g, h) let associative = compose(compose(f, g), h) == compose(f, compose(g, h)) // true
lodash/fp
The fp module of lodash provides a practical and friendly method for functional programming
// lodash module const _ = require('lodash') _.map(['a', 'b', 'c'], _.toUpper) // => ['A', 'B', 'C'] _.map(['a', 'b', 'c']) // => ['a', 'b', 'c'] _.split('Hello World', ' ') // lodash/fp module const fp = require('lodash/fp') fp.map(fp.toUpper, ['a', 'b', 'c']) fp.map(fp.toUpper)(['a', 'b', 'c']) fp.split(' ', 'Hello World') fp.split(' ')('Hello World')
Functor
Functor is the most important data type in functional programming, and it is also the basic operation unit and functional unit.
What is functor
- Container: contains a deformation relationship between values and values (this deformation relationship is a function)
- Functor: it is a special container, which is implemented through an ordinary object. The object has a map method. The map method can run a function to process the value (deformation relationship)
// A container that wraps a value class Container { // of static method, you can omit the new keyword to create an object static of (value) { return new Container(value) } constructor (value) { this._value = value } // Map method, pass in the deformation relationship, and map each value in the container to another container map (fn) { return Container.of(fn(this._value)) } } // test Container.of(3) .map(x => x + 2) .map(x => x * x)
- The operation of functional programming is not directly operated on the value, but completed by the functor
- A functor is an object that implements a map contract
- We can think of a functor as a box in which a value is encapsulated
- To handle the values in the box, we need to pass a value processing function (pure function) to the map method of the box, which processes the values
- Finally, the map method returns a box (functor) containing the new value
MayBe functor
- We may encounter many errors in the process of programming, and we need to deal with these errors accordingly
- The function of MayBe functor is to handle the external null value (control the side effects within the allowable range)
class MayBe { static of (value) { return new MayBe(value) } constructor (value) { this._value = value } // If the null value is deformed, the functor with null value is returned directly map (fn) { return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value)) } isNothing () { return this._value === null || this._value === undefined } } // Pass in specific value MayBe.of('Hello World') .map(x => x.toUpperCase()) // When null is passed in MayBe.of(null) .map(x => x.toUpperCase()) // => MayBe { _value: null }
Either functor
- Either of the two, similar to if... else
- Exceptions will make the function impure. Either functor can be used for exception handling
class Left { static of (value) { return new Left(value) } constructor (value) { this._value = value } map (fn) { return this } } class Right { static of (value) { return new Right(value) } constructor (value) { this._value = value } map(fn) { return Right.of(fn(this._value)) } } function parseJSON(json) { try { return Right.of(JSON.parse(json)); } catch (e) { return Left.of({ error: e.message}); } } let r = parseJSON('{ "name": "zs" }') .map(x => x.name.toUpperCase()) console.log(r)
IO functor
- In IO functor_ Value is a function. Here, the function is treated as a value
- IO functors can store impure actions in_ value, delay the execution of this impure operation (lazy execution), and wrap the current pure operation
- Leave the impure operation to the caller
const fp = require('lodash/fp') class IO { static of (x) { return new IO(function () { return x }) } constructor (fn) { this._value = fn } map (fn) { // Combine the current value and the passed in fn into a new function return new IO(fp.flowRight(fn, this._value)) } } // call let io = IO.of(process).map(p => p.execPath) console.log(io._value())
Task asynchronous execution
folktale is a standard functional programming library
const fs = require('fs'); const { task } = require('folktale/concurrency/task') function readFile(filename) { return task(resolver => { fs.readFile(filename, 'utf-8', (err, data) => { if (err) resolver.reject(err) resolver.resolve(data) }) }) } // Call run to execute readFile('package.json') .map(split('\n')) .map(find(x => x.includes('version'))) .run().listen({ onRejected: err => { console.log(err) }, onResolved: value => { console.log(value) } })