I was going to study the implementation of compose, but I saw a larger world. Let's take a look at functional programming first, and then learn the implementation of compose.
1, Imperative programming VS declarative programming
Common programming paradigms include: imperative programming (process oriented), object-oriented programming (Class), declarative programming, functional programming (a kind of declarative programming), etc.
To achieve (1+2) * 3 / 4, imperative and declarative programming are as follows:
Imperative programming
It focuses on the specific process of how to do it, and the algorithm implemented to solve a certain problem.
// , imperative programming is as follows a = 1 + 2 b = a * 3 c = b / 4
A process to solve a problem. You need to look at the code line by line to understand what you want.
Declarative programming
Focus on goals, what I need.
divide(multiply(add(1, 2), 3), 4)
I need the result after add - > the result after multiply - > the result after divide. While 'how' is left to specific functions, we only focus on what we want.
Declarative programming languages, such as SQL, D3
SQL: what kind of associated data do I want
SELECT * from dogs INNER JOIN owners WHERE dogs.owner_id = owners.id
D3: what I want is a circle with x and y centers. The initial value is 0. After half a second, it will be transformed into a radius of 5
var circles = svg.selectAll('circle') .data(data) circles.enter().append('circle') .attr('cx', function(d) { return d.x }) .attr('cy', function(d) { return d.y }) .attr('r', 0) .transition().duration(500) .attr('r', 5)
Declarative programming makes us focus more on "what" than "how", and the code looks more readable. It also helps us to solve problems at a higher level. In a suitable scenario, we can apply more declarative programming paradigms.
2, Functional programming
Emphasize the one-to-one mapping relationship. For the same input, there will only be the same output. It has the following characteristics:
- Functions are "first-class citizens": the basis of operation is functions, which can be used as input and output parameters for other functions
- Declarative programming: focus on what I want, not how to do it
- Lazy execution: only execute when necessary, with almost no meaningless intermediate variables. Writing functions from beginning to end
- Stateless and data immutable:
Stateless: it does not depend on external states (global variables, this pointer, IO operations, etc.), and the same input is the same output
Immutable data: do not change the original data (do not modify the global and input parameters). If you want to modify an object, you should create a new object to modify it. - No side effect: the side effect is to manipulate external variables at will, which may lead to bug s caused by shared state, but it is difficult to find the source.
- Pure function: independent of external state (stateless), no side effects (data unchanged). Easy to test and optimize (without mutual influence), easy to cache (one input and only one output)
3, Function composition
In functional programming, the two indispensable operations are currying and function combination. Here we mainly talk about function combination.
concept
Combine multiple functions into one to use.
- The first function is multivariate (accepts multiple parameters), and the following functions are all cellular (accepts one parameter)
- Right to left execution sequence
- The execution of all functions is synchronous
let step1 = (val) => val + 1 let step2 = (val) => val + 2 const steps= [step2, step1] const composeFuc = compose(...steps) composeFuc(1) // 4: 1+1=2 -> 2+2=4
Simple version understanding:
const compose = (f, g) => x => f(g(x))
How about multiple parameters?
Mature version:
function compose(...fn) { if (!fn.length) return (v) => v; if (fn.length === 1) return fn[0]; return fn.reduce( (pre, cur) => (...args) => pre(cur(...args)) ); }
analysis:
function a() { console.log(1); } function b() { console.log(2); } function c() { console.log(3); } compose(a, b, c) // In order to get this result a(b(c())) // Execute pre: a, curr: b for the first time d = (...args) => a(b(...args)) // Execute pre: d, cur: c for the second time. Get a(b(c())) (...args) => d(c(...args))
It is more effective when used with Coriolis.
Coritization implementation:
function curry(fn, ...args) { const length = fn.length; let allArgs = [...args]; const res = (...newArgs) => { allArgs = [...allArgs, ...newArgs]; if (allArgs.length === length) { return fn(...allArgs); } else { return res; } }; return res; }
Use together:
const split = curry((x, str) => str.split(x)); const join = curry((x, arr) => arr.join(x)); const replaceSpaceWithComma = compose(join(','), split(' ')); // First pass in an x, and then str. const replaceCommaWithDash = compose(join('-'), split(',')); replaceSpaceWithComma('a b c') // a,b,c
Debug of function combination
const trace = curry((tip, x) => { console.log(tip, x); return x; }); const lastUppder = compose(toUpperCase, head, trace('after reverse'), reverse);
Supplement:
Partial function application vs coriolism
Partial function application: fix some parameters and return a function with smaller elements (pass in fewer parameters).
// Suppose a generic request API const request = (type, url, options) => ... // GET request request('GET', 'http://....') // POST request request('POST', 'http://....') // But after partial call, we can extract the request of a specific type const get = request('GET'); get('http://', {..})
Reference links
https://www.cnblogs.com/sunny-lucky/p/14119172.html
https://juejin.cn/post/6844903936378273799#heading-8
https://juejin.cn/post/6968713283884974088