Knowledge of functional programming

It is the first time to write an article to share what I have learned, and I also want to record what I have learned. If there is a wrong place to write welcome to point out ha ~ if the content of the article is helpful to you, please give a praise ha. As a famous front end white, his FP function understanding certainly can not be fully in place, if the subsequent functional programming and new insights will continue to update the article ~

Preface: Why should we learn Functional Programming? The main front-end frameworks like React and Vue3 are all functional programming, so we have to learn about functional programming.


Since functional programming is a programming paradigm, it also means that it is not like a framework that you can learn after careful study. Reading this article may be more of an inspiration to you. Functional programming is the need for us to constantly practice knowledge in the work, need time to precipitation and accumulation.

Functional programming is a paradigm in programming that is juxtaposed with the style (idea) of object-oriented programming. (Programming paradigm: a programming idea or style of programming in programming)

  • Functional programming: Connecting things to things in the real worldcontactAbstract to the program world (abstract its operations)
    • The essence of the program: according to the input through some operation to obtain the corresponding output, the process of program development will involve a lot of input and output functions
    • The functions of functional programming are the functions of exponentials such as ** y=sin(x)**, the mapping between x and y
    • X ->f ->y, y=f(x)
Base pay + performance pay
function add(a,b) {
	return a + b
}
Copy the code

Preknowledge of functional programming

Functions are First class functions.

Reference to the MDN definition of functions as first-class citizens: a programming language is said to have first-class functions when its functions can be used as variables. For example, in this language, functions can be passed as arguments to other functions, returned as a value to another function, or assigned to a variable.

MDN functions are first-class citizens

// Examples of functions (which are themselves objects) used like objects
// No matter how we declare a Function, it is an instance of Function
// You can declare a function by using the new keyword (ps: not recommended in js advanced programming).
const fn = new Function(a)console.log(fn) // [Function: anonymous]

// Has its own instance properties like an object
const sum = (a, b) = > a + b
console.log(sum.length) / / 2

// Can be stored in an array object
const obj = {
  sum
}
const arr = [sum]
console.log(obj.sum(1.2)) / / 3
console.log(arr[0] (3.5)) / / 8

// as an argument
const callback = (pre,cur) = > pre + cur
console.log([1.2.3.4].reduce(callback,0))

// as the return value
function doOnce(fn) {
  let isDone = false
  return function (money) {
    if (isDone) return
    isDone = true
    fn(money)
  }
}
const pay = doOnce(money= > {
  console.log(` paid${money}$`)})// Multiple calls to pay only perform one print to pay $100
pay(100)
pay(100)
pay(100)
Copy the code

Summarized as:

  • Functions can be used as variables
  • A function can be used as an argument to another function
  • Functions can be returned as values

When I first learned about this concept, I was very confused why do I call functions first class citizens? A function functions just like a normal object, except that it can be called (which makes it a first-class citizen).

But don’t worry too much. It’s just a concept. All we need to do is remember the summary above, and that’s the basis for what we’re going to learn about higher order functions and creatification.

Higher-order functions

Higher-order functions may sound fancy, but they are often used in everyday development. For example, the usual array methods: forEach, map, filter, find, some, etc., are all higher-order functions.

Definition of higher order functions:

  • A function can be used as an argument to a function
  • A function can be the return value of another function

High-order functions often used in work include:

  • forEach
  • map
  • filter
  • every
  • some
  • find/findIndex
  • reduce
  • sort
  • .

The meaning of using higher-order functions:

  • Block out the details and focus on the target
  • The generic problem is abstracted
const arr1 = [1.2.3.4]
const arr2 = [11.12.13.14]
// Now we have two requirements. Find the number greater than 2 in the first array and the number less than 13 in the second array. What do you do?

// Process-oriented programming
// The first requirement
let res1 = []
for (let i = 0; i < arr1.length; i++) {
  if (arr1[i] > 2) {
    res.push(arr[i])
  }
}

// The second requirement
let res2 = []
for (let i = 0; i < arr2.length; i++) {
  if (arr2[i] < 13) {
    res2.push(arr[i])
  }
}

// Use higher-order functions
function myFilter(arr, fn) {
  let res = []
  for (let i = 0; i < arr.length; i++) {
    if (fn(arr[i], i, arr)) {
      res.push(arr[i])
    }
  }
  return res
}
const res1 = myFilter(arr1, itme= > item > 2)
const res2 = myFilter(arr2, item= > item < 13)

// 1. Compared to a procedural implementation requirement, using higher-order functions can help us to mask the details (such as using a for loop) and focus only on our goal (to get the number of arR1 array greater than 2).
// 2. You can abstract generic issues, such as filtering the elements of an array in the current example.
Copy the code

closure

Dig a hole, I will write a close package blog, please continue to pay attention.

Basic functional programming

Pure functions

The concept of a pure function: the same input always yields the same output, without any observable side effectsSame input and same output means the same input x will always give you the same output y. y=sin(x)

// Array slice and splice methods
const arr = [1.2.3.4.5]

/ / pure functions
// Slice is a pure function. It will always get the same output as long as the input is the same no matter how many times it is called.
console.log(arr.slice(0.3)) // [1, 2, 3]
console.log(arr.slice(0.3)) // [1, 2, 3]
console.log(arr.slice(0.3)) // [1, 2, 3]

// Impure function
/ / splice method
console.log(arr.splice(0.2)) // [1, 2]
console.log(arr.splice(0.2)) // [3, 4]
console.log(arr.splice(0.2)) / / [5]
Copy the code

The benefits of pure functions

  1. You can cache the results.
// 
// Memoize in lodash
The memoize function takes an fn as an argument and returns a new function
// Use the closure feature to cache the result of the call, and return the result of the call if the parameters passed in were cached. If it has not been cached, the calling method is executed.
function memoize(fn) {
  let cache = {}
  return function (. args) {
    // Since a pure function will always get the same output from the same input, save the input as the key value.
    let key = JSON.stringify(args)
    if(! cache[key]) { cache[key] = fn(... args) }return cache[key]
  }
}
const newGetAreaFn = memoize(getArea)
console.log(newGetAreaFn(4))
console.log(newGetAreaFn(4))
console.log(newGetAreaFn(4))
console.log(newGetAreaFn(5))
// console:
/ / 234234
/ / 50.26548245743669
/ / 50.26548245743669
/ / 50.26548245743669
/ / 234234
/ / 78.53981633974483
Copy the code
  1. Pure functions do not depend on the shared data of the external environment and can be called from any scope.
  2. Pure functions make testing easier, focusing only on input and output.
  3. Fewer bugs.

A side effect of a pure function is that it has a dependent external state inside of it, and a change in the external state can cause the function to become impure (possibly resulting in the same input not getting the same output). Consider this example.

let mini = 18
// A function that is not pure, which has the obvious side effect that its internal output depends on the state of the external mini.
function checkAge(age) {
  return age >= mini
}

/ / pure
function checkAge(age) {
  let mini = 18
  return age >= mini
}
Copy the code

Side effects may include, but are not limited to:

  • Change the file system, system configuration
  • Insert records into the database
  • Send an HTTP request
  • Variable data
  • Get user input
  • Accessing system Status
  • , etc.

Conclusion: In summary, any interaction with the external environment of a function is likely to bring side effects, and side effects are inevitable. For example, if the input value of your pure function needs to be obtained through the interface, the value returned by the background interface will have various possibilities. It may meet the expected target value, or it may not meet the expected value, which will cause problems within the function. As for side effects it’s not about banning all side effects, it’s about making them manageable.

Lodash module

Is a pure function library that provides methods for manipulating arrays, numbers, objects, strings, functions, and more

Currie,

When a function has more than one argument, call it by passing some of the arguments (which are not changed later) and have it return a new function to handle the rest of the arguments

/ / curry
const _ = require('lodash')
/ / requirements:
// The array is filtered according to different conditions through the currie form
// Generate a match curlization function
The match() method retrieves the result of a string matching the regular expression (the result is an array, or null if it matches).
const match = _.curry((reg, str) = > {
  return str.match(reg)
})
// Call it with some arguments and return a function to handle the rest
const haveSpace = match(/\s+/g)
const haveNumber = match(/\d+/g)
// console.log(haveSpace('hello world')) // ['']
// console.log(haveNumber('hello world')) // null

// Get a filter curlization function
const filter = _.curry((fn, arr) = > {
  return arr.filter(fn)
})
// console.log(filter(haveSpace, ['hello world', 'helloooooo'])) // [ 'hello world' ]

// Get a method that matches the empty string in the array
const findSpace = filter(haveSpace)

console.log(findSpace(['helloMolly'.'reborn jiang']))


Copy the code

Simulation of the implementation of Lodash Currying

/ / analysis:
// 1. Curry accepts a function callback and returns a function.
// 2. The number of arguments in the return function is compared to the number of arguments in the callback function. If the number of arguments is equal to the number of arguments in the callback function, call the function directly.
// If the number of parameters in the return function is less than the number of parameters in the callback function, then the return function continues to wait until the remaining parameters are accepted
// This function is called if the parameters are equal.

function myCurry(fn) {
  return function curry(. args) {
    // 1. Return the number of parameters to compare with the fn function
    if (args.length < fn.length) {
      // 3. Return the function and wait for the remaining arguments
      return function () {
        // 4. If the number of parameters in the second return function is smaller than the number of parameters in the first return function, we need to recursively call curry until all the remaining parameters have been passed and the function is executed.
        let curTotalParmams = args.concat(Array.from(arguments))
        returncurTotalParmams.length < fn.length ? curry(... curTotalParmams) : fn.apply(this, curTotalParmams)
      }
    } else {
      // 2. The return function is called if the number of its arguments may be greater than or equal to the number of fn's arguments
      return fn.apply(this, args)
    }
  }
}

// Simplify the code
function myCurry(fn) {
  return function curryFn(. args) {
    if (args.length < fn.length) {
      return function () {
        returncurryFn(... args.concat(Array.from(arguments)))}}returnfn(... args) } }// Test a wave
function add(a, b, c, d) {
  return a + b + c + d
}

// Convert the add function to the curry function
const curryAdd = myCurry(add)
console.log(curryAdd(1) (2) (3) (4)) / / 10
Copy the code

In the curry case above, it seems that the complexity of the code is increased by currying requirements back in the curry way, so what is the benefit of currying functions? When you look at the combination of functions, you’ll see.

The benefits of currying functions

  • Through curry, functions of multiple variables can be converted into functions of one variable, which makes functions more flexible and less granular.
  • The compose function is used to compose more powerful functions.

Function composition

The concept of function combination: if a function has to be processed by multiple functions to get the final value, it can be combined into a single function.

  • The functions to be combined must be pure functions and functions of fineness.

I’m going to give you a little bit of a little bit of an example and a little bit of a little bit of a diagram to help you understand the combination of functions.

  • For example,

Functions are like factories, where the inputs are raw materials and the outputs are goods. Raw materials need to go through a process to get to the final product. Each processing process is a function, many processing processes constitute a production line. The combination of functions is the production line.

/ / pseudo code
function factory(rawMaterials) {
  // Input raw materials
  
  // Process oriented
  // Raw material processing
  // After processing the raw material n times here
  const res1 = process1(rawMaterials)
  const res2 = process2(res1)
  / /...
  const product = process3(resN)
  
 / / function type
 // Take the form of function combination
  constproductLine = compose(process1, process2, ... , processN)const product = productLine(rawMaterials)
  

  // Export goods
  return product
}
Copy the code
  • Here’s a diagram (using function ratios as conduits)

The pipeThe following graph shows the process of using a function to process data in a program. Input a to fn and return b as a result. So you can think about data A going through a pipe to data B.When fn is more complicated, we can break it up into smaller functions, with more m and n generated by the intermediate operations. In the figure below, you can imagine that the pipe FN is divided into three pipes F1, F2 and F3. Data A goes through the pipe F3 to get the result M, m goes through the pipe F2 to get the result N, and n goes through the pipe F1 to get the final result B

Compose function in lodash:

  • The combined function flow() or flowRight() in odash, both of which can combine multiple functions.
  • Flow () runs from left to right
  • FlowRight () runs from right to left and uses more
const fp = require('lodash/fp')

// Requirement: take the last element of the array and convert it to uppercase
let arr = ['rebornjiang'.'helloworld']

function reverse(arr) {
  return fp.reverse(arr)
}

function getFirstEl(arr) {
  return arr[0]}function upperCase(str) {
  return fp.upperCase(str)
}
// We get the three pure functions above, using the function combination method provided in lodash

// flow && flowRight both get the same result
// const getUpperCaseFromArr = fp.flow(reverse,getFirstEl, upperCase)
const getUpperCaseFromArr = fp.flowRight(upperCase,getFirstEl, reverse)
console.log(getUpperCaseFromArr(arr)) // HELLOWORLD
Copy the code

Implement your own method of function composition

const fp = require('lodash/fp')
let arr = ['rebornjiang'.'helloworld']

function reverse(arr) {
  return fp.reverse(arr)
}
function getFirstEl(arr) {
  return arr[0]}function upperCase(str) {
  return fp.upperCase(str)
}

/ / analysis:
For example, compose accepts multiple functions and returns a composite function.
// When a combined function is called, each function is called sequentially, taking the return value of the previous function as an input to the next function, and returning the result after the last function is executed
function compose(. args) {
  return function (val) {
    // Simulate flowRight and reverse the array.
    return args.reverse().reduce((accumulator, curFn) = > {
      return curFn(accumulator)
    }, val)
  }
}
const getResult = compose(upperCase, getFirstEl, reverse)

console.log(getResult(arr)) // HELLOWORLD
Copy the code

The associative law of combination of functions is understood in accordance with the associative law of multiplication in primary school mathematics (when three numbers are multiplied, the first two numbers are multiplied, and then the other number is multiplied, or the last two numbers are multiplied, and then the other number is multiplied, and the product is unchanged, which is called the associative law of multiplication).

// For example above:
// Combine the latter two methods again. The result is the same. This is the associative law of the combination of functions
const getResult = compose(compose(upperCase, getFirstEl), reverse)
console.log(getResult(arr)) // HELLOWORLD
Copy the code

Debugging of function combinations after function combinations, how do you debug that?

// Use trace currying function
const trace = fp.curry((msg, val) = > {
  console.log(msg, val)
  return val
})

const getResult = compose(compose(upperCase, getFirstEl), trace('after the reverse'), reverse)

console.log(getResult(arr)) 
// Reverse after [' helloWorld ', 'rebornjiang']
// HELLOWORLD
Copy the code

functor

Personal understanding of functors is very one-sided, the query of other data is still obscure. Functors compared to function curitisation, function combination, the PointFree programming style is easy to use in real projects and hard to use in real development. Individuals do not waste too much time on this, waiting for the subsequent individual to have new insights to update.

Why use functors? To control the side effects in functional programming in a controllable range, exception handling, asynchronous operation, etc.

What is a functor? A functor is like a box (object) to keep the value under control. The object has a map method that takes a function to process the value.

// A simple functor to manipulate values using the map method.
class Container {
  constructor(value) {
    this._value = value
  }

  map(fn) {
    return new Container(fn(this._value))
  }
}


console.log(new Container(3).map(x= > x+2)) // Container { _value: 5 }
Copy the code

conclusion

  • The operations of functional programming do not operate directly on values, but are performed by functors.
  • Functors are objects that implement the MAP contract
  • We can think of a functor as a box, and the box encloses a value
  • To process the value in the box, we need to pass a function (pure function) to the box’s map method to process the value
  • Finally, the map method returns a box (functor) containing the new value

The purpose of the MayBe functor is to solve the problem of abnormal errors caused by passing null values externally in functional programming, in other words, to control the side effects within the permissible range.

/ / maybe functor
class MayBe {
  // Implicitly new an object to avoid confusion with the object-oriented programming style.
  static of(val) {
    return new MayBe(val)
  }
  constructor(value) {
    this._value = value
  }

  map(fn) {
    return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
  }
  // Check whether the value is null
  isNothing() {
    return this._value === null || this._value === undefined}}Log (container.of (null).map(item => item.toupperCase ())) // Exception
console.log(MayBe.of(null).map(item= > item.toUpperCase)) // MayBe { _value: null }
Copy the code

Either the functor is one of two, similar to if.. The else. The purpose of Either is to do code exception handling.

// Requirement: intercepts json.parse () error parsing non-JSON string
class Left {
  static of(value) {
    return new Left(value)
  }
  constructor(value) {
    this._value = value
  }
  map(fn) {
    return this}}class Right {
  static of(val) {
    return new Right(val)
  }
  constructor(value) {
    this._value = value
  }
  map(fn) {
    return Right.of(fn(this._value))
  }
}

function parseJson(json) {
  try {
    return Right.of(JSON.parse(json))
  } catch (err) {
    return Left.of({ error: err })
  }
}

// console.log(parseJson({name: 323})) // Catch an exception

console.log(parseJson('{"name":"reborn"}')) // Right { _value: { name: 'reborn' } }
Copy the code

IO functor The IO functor has the following characteristics:

  • This_value holds a function that returns the value of the functor inside the function. The purpose of this is to delay the execution of the function, leaving the impure operation to the caller of the function, thus ensuring that the current operation is pure.
const fp = require('loadsh/fp')
/ / IO functor
class IO {
  static of(value) {
    return new IO(function () {
      return value
    })
  }
  constructor(fn) {
    this._value = fn
  }
  // Note that the IO functor instance returned by the IO functor map is not created implicitly by using of, but directly by using the new keyword.
  // There is a problem of nesting multiple levels of functions through of.
  map(fn) {
    return new IO(fp.flowRight(fn, this._value))
  }
}

/ / call
const io = IO.of(process).map(item= > process.execPath)
console.log(io._value())
Copy the code

Monad functor

  • The Monad functor is a margin functor that can be flattened (the one with the OF method is the margin functor) in order to solve the problem of IO(IO(x)) functor nesting.
  • A functor is a Monad if it has join and of methods and obeies some laws
const fp = require('loadsh/fp')
const fs = require('fs')
/ / Monad functor
// Requirements: Read the package.json file and print it
class IO {
  static of(value) {
    return new IO(function () {
      return value
    })
  }
  constructor(fn) {
    this._value = fn
  }
  map(fn) {
    return new IO(fp.flowRight(fn, this._value))
  }

  // Add a prototype method for the IO functor
  join() {
    return this._value()
  }

  // Call the map and join methods
  flatMap(fn) {
    return this.map(fn).join()
  }
}

/ / call
function readFile(path) {
  return new IO(function () {
    return fs.readFileSync(path, 'utf-8')})}function print(val) {
  return new IO(function () {
    console.log(val)
    return val
  })
}

// const readFileAndPrint = fp.flowRight(print, readFile)
// After calling readFileAndPrint, we pass the IO functor returned by the readFile method to the print method as an argument
// The print method returns an IO functor, and the _value method returns an IO functor for the read operation.
// console.log(readFileAndPrint('package.json')._value()._value())

// Use monad functor to solve the IO functor nesting problem
/ / resolution:
// The call to readFile returns an IO functor containing the operation of reading the file. The call to map performs the operation of reading the file with the operation of uppercase
// Return a new functor. Call flatMap to combine the functor print method with the functor of the combined method to get a new one
// The function contains the read file & uppercase & print. The join function in flatMap executes to get the I/o functor returned by the print method.
// Then call join to print the contents of the file with caps turned
const res = readFile('package.json').map(fp.toUpper).flatMap(print).join()
console.log(res)
Copy the code

Task functors Task functors are designed to handle tasks in the asynchronous operation Folktale

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)
}
})
Copy the code

Reference: Functional programming guide netease Cloud music team functional programming article Ruan Yifeng teacher functional programming introduction Ruan Yifeng teacher PointFree programming style Ruan Yifeng teacher diagram Monad