introduce
Function Programming (FP) is one of the Programming paradigms (Programming paradigms refer to the canonical pattern or method of Programming in computers). We often hear of object-oriented Programming paradigms, procedural Programming.
So what is functional programming? Let’s compare OO(object-oriented) programming with FP(functional) programming
- OO thinking: Abstracts things from the real world into classes and objects in the program world, and demonstrates the relationships between things through encapsulation and inheritance polymorphism.
- FP way of thinking: abstract the connections between things in the real world to the program world, that is, toprocessEncapsulate.
- FP is a pure function; the same input always yields the same output
- FP is used to describe mappings between data (functions)
- A function in FP is not a function (method) in a program, but a function (mapping) in mathematics, for example: y= sin(x), the relation between x and y
Let’s look at a simple piece of code:
// Non-functional
let num1 = 2
let num2 = 3
let sum = num1 + num2
console.log(sum)
/ / function type
function add (n1, n2) {
return n1 + n2
}
let sum = add(2.3)
console.log(sum)
Copy the code
In the above code we can clearly see that non-functional is a process of programming code, and FP encapsulates it. Let’s look at some of the features of functional programming, including pure functions, Currization, and so on
Pure functions
Before introducing pure functions, let’s understand what functions are: we all know that functions are first-class citizens. Why? Because functions can be stored as values in variables or arrays, or as arguments and return values to another function.
Pure function concept
The same input always gets the same output without any side effects (what are side effects are described below) similar to the mapping y = sin(X) in mathematics
- Slice is a pure function. Slice takes some values from the current array and returns them, but does not alter the original array
- Splice is not a pure function. First, it changes the value of the array. If you call splice multiple times on the same array, it will get different values
So what are the benefits of pure functions?
- Cacheable Because the same input always has the same output, you can store the value returned by the function
- Testable pure functions make testing easier
- Parallel processing If it is not a JS language, like Java, multi-threaded parallel processing of shared data can easily lead to data errors, pure functions never change the input value, so there is no problem.
Side effects
Look at a piece of code
/ / not pure
let mini = 18
function checkAge (age) {
return age >= mini
}
// Pure (hardcoded, later can be solved by Currization)
function checkAge (age) {
let mini = 18
return age >= mini
}
Copy the code
If the first function has an external variable inside it, it may result in a different value each time the same checkAge is executed. If a function depends on external state and cannot be guaranteed output, there are side effects. And side effects make functions less malleable, but side effects cannot be completely banned, we can control it within a certain range.
Currie,
Let’s look at a scenario
function checkAge (age, min) {
return age >= min
}
Copy the code
So we have a checkAge method that checks if someone is older than a certain age so let’s say we have 5 people, 3 of them are 18 years old, check if one of them is older than 20, one of them is older than 22, and one of them is older than 33 and then we call
checkAge(18.20)
checkAge(18.22)
checkAge(18.33)
Copy the code
So what you can see is 18 we’re doing it over and over again, and the whole point of coriation is to simplify this
function checkAge(age) {
return function (min) {
return age >= min
}
}
const check18 = checkAge(18)
check18(22)
check18(30)
check18(33)
Copy the code
It is also easier to use by saving an 18 year old person and checking min at a time. Currization When a function has multiple arguments, it is called by passing some of the arguments (the arguments never change) and then returns a new function to receive the rest of the arguments, or it can return some of the arguments as a new function until all the arguments are passed and the result is returned.
const _ = require('lodash')
// The currified function
function getSum (a, b, c) {
return a + b + c
}
// The currie function
let curried = _.curry(getSum)
/ / test
curried(1.2.3)
curried(1) (2) (3)
curried(1.2) (3)
Copy the code
Here we take the Creerization of Lodash as an example. Here is a simulation of creerization
function myCurried(fn) {
// Declare an array to hold parameters
let arr = []
Fn. length can be used to obtain the number of parameters that fn needs to accept
let len = fn.length
return function a(. args1) {
// Push to ARR each time an argument is receivedarr.push(... args1)// If the number of arguments in the array is less than len, then return the function again (recursively)
if (arr.length < len) {
return a
}
// Call fn when len is equal and return the result
returnfn(... arr.splice(0))}}Copy the code
Conclusion:
- Currization can pass fewer arguments and return a function that ‘remembers’ some of the arguments
- Currization is a cache of parameters
- Currization makes the function more granular and flexible
Higher-order functions
- You can pass a function as an argument to another function
- You can treat a function as the return result of another function
Note: one of the above two satisfies is the higher-order function function as an argument
// forEach
function forEach(array, fn) {
for (let i = 0; i < array.length; i++) {
fn(array[i])
}
}
Copy the code
A simple implementation inside foreach, passing fn in as an argument, executing inside foreach, this is a higher-order function, which I thought was the only higher-order function that returns a value as an argument
// once
function once(fn) {
let done = false
return function () {
if(! done) { done =true
return fn.apply(this.arguments)}}}let pay = once(function (money) {
console.log(` pay:${money} RMB`)})// Will only be paid once
pay(5)
pay(5)
pay(5)
Copy the code
Advantages of using higher-order functions:
- Mask the details of the function’s internal processing and focus only on the target
- Higher-order functions usually use abstract general-purpose problems.
- ForEach, map, filter, every, some, find/findIndex, reduce, sort…
closure
I covered closures a long time ago, but here’s a quick note about closures: a function is bundled together with references to its surrounding state (lexical context) to form a Closure. You can call an inner function of a function from another scope and access members of that function’s scope
// function as return value
function makeFn () {
let msg = 'Hello function'
return function () {
console.log(msg)
}
}
const fn = makeFn()
// When fn() is called, there is a closure. The inner function accesses the variable of the outer function. MSG cannot be released because fn is called
fn()
Copy the code
The essence of closures: functions are placed on an execution stack at execution time and removed from the stack when the function completes execution, but scoped members on the heap cannot be freed because of external references, so internal functions can still access the members of the external function
Functor (Factor)
Why should we know functors? We know some of the basics of functional programming, but we don’t yet know how to keep side effects under control, asynchronous operations, exception handling, etc. This part is selected for reading, but I don’t know much about it.
What is a functor
A functor is a container that contains a value and its handling of the value (that handling is a function). A functor is a special container that is implemented by an object that has a map method that runs a function that changes the value passed.
Factor functor
// A container that wraps a value
class Container {
// of static methods that create objects without the new keyword
static of(value) {
return new Container(value)
}
constructor(value) {
this._value = value
}
// the map method, passing in the deformation relation, maps 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)
Copy the code
- The operations of functional programming do not operate directly on values, but are performed by functors
- A functor is an object that implements the Map contract
- We can think of a functor as a box that encapsulates a value
- To process the values in the box, we need to pass a value-handling function (pure function) to the box’s Map method, which will process the values
- The final map method returns a box containing the new value (functor)
One drawback of Factor is that when a value is passed in as null, an error is reported by a map operation, for example
// value if accidentally passed a null value (side effect)
Container.of(null)
.map(x= > x.toUpperCase())
// TypeError: Cannot read property 'toUpperCase' of null
Copy the code
MayBe functor
We may meet many errors in the process of programming, and we need to deal with these errors accordingly. The function of the MayBe functor is to handle the external null case (control the side effect within the allowed range), and it is relatively easy to handle, just need to add a judgment line, if null or undefined return null
/ / MayBe functor
class MayBe {
static of (value) {
return new MayBe(value)
}
constructor (value) {
this._value = value
}
// Return a null functor if the null value is distorted
map (fn) {
return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
}
isNothing () {
return this._value === null || this._value === undefined}}Copy the code
In the MayBe functor it is difficult to determine which step went wrong, for example
MayBe.of('hello world')
.map(x= > x.toUpperCase())
.map(x= > null)
.map(x= > x.split(' '))
// => MayBe { _value: null }
Copy the code
Either functor
- Either of the two, similar to if… else… The processing of
- Exceptions make functions impure, and Either functors can be used for exception handling
// Return the original functor to catch exceptions
class Left {
static of (value) {
return new Left(value)
}
constructor (value) {
this._value = value
}
map (fn) {
return this}}// Used to execute the input fn when there are no exceptions and return a functor
class Right {
static of (value) {
return new Right(value)
}
constructor (value) {
this._value = value
}
map(fn) {
return Right.of(fn(this._value))
}
}
Copy the code
Solve the maybe functor that cannot determine where NULL occurs. Not only does it handle null where it occurs, it can also be used to get information about where and where the exception occurs. For example,
function parseJSON (str) {
try {
return Right.of(JSON.parse(str))
} catch (e) {
return Left.of({ error: e.message })
}
}
let r = parseJSON('{ name: zs }')
// if r is normal, it outputs normal values
console.log(r)
Copy the code
IO functor
- The _value in the IO functor is a function, and we’re treating the function as a value
- IO functors can store impure actions in _value, delay the execution of the impure operation (lazy execution), and ensure that the current operation is pure
- Leave impure operations 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 incoming 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())
Copy the code
Conclusion:
Functional programming is getting more attention as React becomes more popular, Vue3 is also starting to embrace functional programming. Functional programming can throw this away, it’s a better tree shaking. There are many libraries to look at, like ‘Lodash/FP’ in Loadsh that are all functional programming.