preface
With React’s popularity, functional programming is getting a lot of attention on the front end. In particular, in recent years, more and more libraries have been leaning toward functional development: Lodash/FP, rx.js, Redux’s pure functions, hooks from React16.8, composition Api from Vue3.0… It is also reflected in THE ES5/ES6 standard, such as arrow function, iterator, Map, Filter, Reduce, etc.
So why use functional programming? Let’s take an example: in business requirements development, we are more on the data processing, such as: string array classification, into string object format.
// jsList => jsObj
const jsList = [
'es5:forEach'.'es5:map'.'es5:filter'.'es6:find'.'es6:findIndex'.'add'
]
const jsObj = {
es5: ["forEach"."map"."filter"].es6: ["find"."findIndex"]}Copy the code
Let’s do it with our most common imperative:
const jsObj = {}
for (let i = 0; i < jsList.length; i++) {
const item = jsList[i];
const [vesion, apiName] = item.split(":")
if (apiName) {
if (!jsObj[vesion]) {
jsObj[vesion] = []
}
jsObj[vesion].push(apiName);
}
}
Copy the code
Let’s look at the implementation of the function:
const jsObj = jsList
.map(item= > item.split(':'))
.filter(arr= > arr.length === 2)
.reduce((obj, item) = > {
const [version, apiName] = item
return {
...obj,
[version]: [...(obj[version] || []), apiName]
}
}, {})
Copy the code
When you compare the two pieces of code, you can see that the imperative implementation creates a lot of temporary variables and involves a lot of logic processing, and usually you don’t know what you’re doing until you read the entire piece of code. If the requirements change later, more logical processing will be added, even thinking of the brain pain…
Look at functional implementations: you can see what you’re doing by looking at each function, and the code is more semantic and readable. The whole process is like a complete pipeline, data from one function input, processing is finished to the next processing function… Each function does its own job.
Next, let’s take a quick look at the programming paradigm mentioned above before we dive into the world of functional programming.
Programming paradigm
A programming paradigm refers to a typical programming style in software engineering that provides and determines how programmers view programs.
In object-oriented programming, for example, programmers think of programs as a series of interacting objects; In functional programming, the program is computed as a sequence of stateless functions. A common programming paradigm is as follows:
Imperative programming
Imperative programming is a programming paradigm that describes the behavior required by a computer. It is also the most widely used programming paradigm at present. Its main idea is to think from the point of view of a computer and pay attention to the steps of computing execution, and each step is an instruction. (Representative: C, C++, Java)
Most imperative programming languages support four basic statements:
- Operational statement;
- Loop statements (for, while);
- Conditional branch statements (if else, switch);
- Unconditional branch statements (return, break, continue).
Every step the computer performs is controlled by the programmer, so it can control the code more carefully and improve the performance of the application program. But because of the existence of a large number of process control statements, it is easy to cause logic disorder when dealing with multi-threaded and concurrent problems.
Declarative programming
Declarative programming describes the nature of the goal so that the computer understands the goal, not the process. By defining specific rules, the underlying system can automatically implement specific functions. (Rep: Haskell)
Compared with imperative programming paradigm, there is no need for flow control language and no redundant operation steps, which makes the code more semantic and reduces the complexity of the code. But the logic of the underlying implementation is not controllable and is not suitable for more elaborate code optimizations.
In conclusion, the biggest difference between the two programming paradigms is:
- How: Imperative programming tells the computer
How to
Computing, concerned with the steps to solve a problem; - WhatDeclarative programming tells the computer to compute
what
Be concerned with the goal of solving the problem.
Functional programming
Declarative programming is a big concept that includes some well-known subprogramming paradigms: constrained programming, domain-specific languages, logical programming, and functional programming. Domain-specific languages (DSLS) and functional programming (FP) are more widely used in the front end, which brings us to today’s main protagonist — functional programming.
Functional programming is not a tool, but rather a programming idea that can be applied to any environment. It is a software development style that focuses on the use of functions. This is quite different from the way you are familiar with object-oriented programming thinking. The purpose of functional is to abstract the operation of the data flow through functions, thereby eliminating side effects and reducing state changes in the system.
To fully understand functional programming, what are its basic concepts?
concept
Functions are first-class citizens
Functions, like other data types, can be assigned to variables, passed as arguments, or returned as a function value. Such as:
// as a variable
fn = () = > {}
// as a parameter
function fn1(fn){fn()}
// As a function return value
function fn2(){return () = >{}}Copy the code
It is the premise that functions are ‘first class citizens’ that makes functional programming possible, and in JavaScript, closures and higher-order functions are the backbone.
Pure functions
A pure function is one in which the same input always gives the same output without any observable side effects.
Now, if you’re familiar with Redux, if you’re familiar with pure functions, all of the modifications in Redux require pure functions. Pure functions have the following characteristics:
- Stateless: The output of a function depends only on the input, not on the external state;
- No side effects: no changes beyond its scope, i.e., no changes to function parameters or global variables.
function add(obj) {
obj.num += 1
return obj
}
const obj = {num: 1}
add(obj)
console.log(obj)
// { num: 2 }
Copy the code
This function is not pure, because the JS object is passing the reference address, and changes inside the function directly affect external variables, resulting in unexpected results. Next, let’s change to pure function writing:
function add(obj) {
const_obj = {... obj} _obj.num +=1
return _obj
}
const obj = {num: 1}
add(obj)
console.log(obj);
// { num: 1 }
Copy the code
This can be done by creating a new variable inside the function (do you remember redux ~~) to avoid side effects. In addition to having no side effects, pure functions have other benefits:
- Cacheability is due to the stateless nature of functional declarations: the same input always yields the same output. So we can cache the result of the function ahead of time, so we can do more. For example, recursive solutions to optimize Fibonacci sequence.
- The dependencies on portable/self-documenting pure functions are clear, easier to observe and understand, and the combination of type signatures makes the program easier to read.
// get :: a -> a
const get = function (id) { return id}
// map :: (a -> b) -> [a] -> [b]
const map = curry(function (f, res){
return res.map(f)
})
Copy the code
- Testability pure functions make testing easier by simply giving an input to a function and asserting the output.
Side effects
A side effect of a function is the effect that occurs when the function is called in addition to returning the value of the function. For example, modify parameters or global variables in the previous example. In addition, the following side effects may occur:
- Changing global variables
- Processing user input
- Screen print or print log
- DOM query and browser cookie, localstorage query
- Sending an HTTP request
- Throws an exception not caught by the current function
- .
Side effects often affect the readability and complexity of the code, leading to unexpected bugs. In actual development, we are inseparable from side effects, so in functional programming should try to reduce side effects, as far as possible to write pure functions.
Reference transparent
A function is said to be reference transparent if it consistently produces the same output for the same output, independent of changes in the external environment.
Data immutable
All data is created and cannot be changed. If you want to change a variable, you need to create a new object to change it (as in the example above for pure functions).
With these concepts out of the way, let’s look at some common operations in functional programming.
Curry
Transform a function that takes multiple arguments into a function that takes a single argument, and return a new function that takes the rest of the arguments and returns the result.
F(a,b,c) => F(a)(b)(c)
Copy the code
Next we implement a simple version of the Curry function.
function curry(targetFunc) {
// Get the number of parameters for the target function
const argsLen = targetFunc.length
return function func(. rest) {
return rest.length < argsLen ? func.bind(null. rest) : targetFunc.apply(null, rest)
}
}
function add(a,b,c,d) {
return a + b + c + d
}
console.log(curry(add)(1) (2) (3) (4));
console.log(curry(add)(1.2) (3) (4));
/ / 10
Copy the code
Careful students may have noticed that the curry function implemented above is not a pure Curry function, because curry emphasizes generating unit functions, but passing in multiple parameters at a time can also be, more like a comprehensive application of Curry and partial functions. How do we define the partial function?
A Partial function is a function that fixes some parameters of a function and then produces another function with smaller elements.
Partial functions can also be created with default partials, similar to bind. Normally, we don’t write our own curry functions. Libraries like Lodash and Ramda implement curry functions, and these libraries implement different definitions of curry functions and currying.
const add = function (a, b, c) {return a + b + c}
const curried = _.curry(add)
curried(1) (2) (3)
curried(1.2) (3)
curried(1.2.3)
// We also implement placeholders for additional parameters
curried(1) (_,3) (2)
Copy the code
Compose
Compose is also an important idea in functional programming. Breaking complex logic down into simple tasks and combining them to complete them makes the data flow more clear, controllable, and readable. This also confirms what we mentioned above: functional programming is like a pipeline, where the initial data is processed by multiple functions, and the overall output is completed.
// The whole process is processed
a => fn= > b
// Split into multiple segments
a => fn1= > fn2= > fn3= > b
Copy the code
Next, we implement the generally simple compose:
function compose(. fns) {
return fns.reduce((a,b) = > {
return (. args) = > {
returna(b(... args)) } }) }function fn1(a) {
console.log('fn1: ', a);
return a+1
}
function fn2(a) {
console.log('fn2: ', a);
return a+1
}
function fn3(a) {
console.log('fn3: ', a);
return a+1
}
console.log(compose(fn1, fn2, fn3)(1));
// fn3: 1
// fn2: 2
// fn1: 3
/ / 4
Copy the code
Examining the above implementation of compose, we can see that fn3 executes before fn2, which executes before fn1. In other words, compose creates a data stream that executes from right to left. If you want to stream data from left to right, you can simply change part of the code for Compose:
- Replace the Api interface
reduce
Instead ofreduceRight
- Interactive package location: Put
a(b(... args))
Instead ofb(a(... args))
.
You can also use the combination provided in Ramda: Pipes.
R.pipe(fn1, fn2, fn3)
Copy the code
The combination of functions not only makes the code more readable, but also makes the overall flow of data clearer and more controllable. Next, let’s look at functional programming in practice in a specific business.
Programming practice
The data processing
In the process of business development, we mostly deal with interface request data or form submission data, especially those who often develop B side. The author has done the processing requirements for a large amount of form data before, for example, to do certain processing for the form data submitted by users: 1. Clear Spaces; 2. All caps.
First, let’s analyze the requirements from the perspective of functional programming:
- Abstract: Each process is a pure function
- Compose: Compose each handler
- Extension: Simply remove or add the corresponding processing pure function
Next, let’s look at the overall implementation:
// 1. Implement the traversal function
function traverse (obj, handler) {
if (typeofobj ! = ='object') return handler(obj)
const copy = {}
Object.keys(obj).forEach(key= > {
copy[key] = traverse(obj[key], handler)
})
return copy
}
// 2. Pure functions that implement specific business processing
function toUpperCase(str) {
return str.toUpperCase() // Convert to uppercase
}
function toTrim(str) {
return str.trim() // Delete the space before and after
}
// 3. Execute by compose
// User submit data as follows:
const obj = {
info: {
name: ' asyncguo '
},
address: {
province: 'beijing'.city: 'beijing'.area: 'haidian'}}console.log(traverse(obj, compose(toUpperCase, toTrim)));
/** { info: { name: 'ASYNCGUO' }, address: { province: 'BEIJING', city: 'BEIJING', area: 'HAIDIAN' } } */
Copy the code
Redux middleware implementation
When it comes to functional practices in JavaScript, redux has to be talked about. First, let’s implement a simple redux version:
function createStore(reducer) {
let currentState
let listeners = []
function getState() {
return currentState
}
function dispatch(action) {
currentState = reducer(currentState, action)
listeners.map(listener= > {
listener()
})
return action
}
function subscribe(cb) {
listeners.push(cb)
return () = > {}
}
dispatch({type: 'ZZZZZZZZZZ'})
return {
getState,
dispatch,
subscribe
}
}
// An application example is as follows:
function reducer(state = 0, action) {
switch (action.type) {
case 'ADD':
return state + 1
case 'MINUS':
return state - 1
default:
return state
}
}
const store = createStore(reducer)
console.log(store);
store.subscribe(() = > {
console.log('change');
})
console.log(store.getState());
console.log(store.dispatch({type: 'ADD'}));
console.log(store.getState());
Copy the code
First, use the Reducer to initialize the store. When subsequent events are generated, use Dispatch to update the store status, and use getState to obtain the latest store status.
Redux defines one-way data flow. Action can only be dispatched by the dispatch function, and the state is updated by the pure function reducer, and then wait for the next event. This one-way data flow mechanism further simplifies the complexity of event management, and middleware can also be inserted into the event flow. Through middleware, a series of extended processes, such as logging, Thunk, asynchronous processing, etc. can be implemented to greatly enhance the flexibility of event processing.
The redux above is further enhanced and optimized:
/ / extension createStore
function createStore(reducer, enhancer){
if (enhancer) {
return enhancer(createStore)(reducer)
}
...
}
// Middleware implementation
function applyMiddleware(. middlewares) {
return function (createStore) {
return function (reducer) {
const store = createStore(reducer)
let _dispatch = store.dispatch
const middlewareApi = {
getState: store.getState,
dispatch: action= > _dispatch(action)
}
// Obtain the middleware array: [mid1, mid2]
// mid1 = next1 => action1 => {}
// mid2 = next2 => action2 => {}
const midChain = middlewares.map(mid= > mid(middlewareApi))
// Get the final dispatch by the compose composition middleware: MID1 (MID2 (mid3()))
Compse Execution sequence: next2 => next1
// 2. Finally dispatch: Action1 (action1 calls next, returns to the previous middleware Action2; When next is called in Action2, go back to the original dispatch)_dispatch = compose(... midChain)(store.dispatch)return {
...store,
dispatch: _dispatch
}
}
}
}
// Customize the middleware template
const middleaware = store= > next= > action= > {
/ /... Logical processing
next(action)
}
Copy the code
Compose all the middleware and return it to the wrapped Dispatch. Next, at each dispatch, the action carries out a series of operations through all the middleware, and finally passes to the pure function Reducer for real state update. Anything middleware can do, we can do by wrapping dispatch calls manually, but having them in one place makes it easier to scale the entire project.
// 1. Manually wrap the dispatch call to implement the Logger function
function dispatchWithLog(store, action) {
console.log('dispatching', action)
store.dispatch(action)
console.log('next state', store.getState())
}
dispatchWithLog(store, {type: 'ADD'})
// 2. Middleware mode packaging dispatch call
const store = new Store(reducer, applyMiddleware(thunkMiddleware, loggerMiddleware))
store.dispatch(() = > {
setTimeout(() = > {
store.dispatch({type: 'ADD'})},2000)})// Middleware execution process
thunk => logger= > store.dispatch
Copy the code
RxJS
When it comes to Rxjs, more people think of Reactive Programming (RP), that is, Programming using asynchronous data streams. Responsive programming uses RX. Observale to provide a unified concept called an observeale stream for asynchronous data. The world of responsive programming is the world of streams. To extract its value, you must first subscribe to it. Such as:
Rx.observale.of(1.2.3.4.5)
.filter(x= > x%2! = =0)
.map(x= > x * x)
.subscrible(x= > console.log(`ext: ${x}`))
Copy the code
From the above examples, we can find that responsive programming is to let the entire programming process flow, like a pipeline, while functional programming is the main, that is, every process of pipeline is no side effects (pure function). Therefore, it is more accurate to say that Rxjs should be Functional Reactive Programming (FRP). As the name implies, FRP has the characteristics of both Functional and Reactive Programming. Today is mainly about functional programming, more Rxjs part of the content, interested students can learn about it. Rxjs is used to process asynchronous data streams.
conclusion
Functional programming is a big topic, and today we mainly introduce the basic concepts of functional programming, as well as more advanced concepts: Functor, Monad, Application Functor, etc., have not been mentioned yet. To truly master these things, we still need some practice and accumulation. Students who are interested in them can learn about them by themselves, or look forward to the author’s subsequent articles.
Compared to object-oriented programming, we can summarize the advantages of functional programming:
- The code is more concise and the process more manageable
- Streaming data
- Reduce the complexity of event-driven code
Of course, functional programming also has certain performance problems, often at the abstraction level because of excessive packaging, resulting in the performance cost of context switching; Intermediate variables also consume more memory space because data is immutable.
In daily business development, functional programming and object-oriented programming should exist in a complementary form, according to the specific needs of the appropriate programming paradigm. In the face of a new technology or new programming method, if its advantages are worth learning and reference, we should not blindly reject it because of a defect, but more often we should be able to think of a better complementary solution. Not to be happy with excellence, not to be sad with inferiority, and you share
Recommended data
functional light JS
Functional-Light-JS – github
redux-middleware
Analysis of functional programming
Functional programming in Redux/React
Functional programming refers to north
A guide to functional programming in JavaScript
Thank you for reading. If you have any questions, please feel free to comment and learn from each other.