II. More detailed functional programming

Posted by ozzthegod on Sat, 18 Dec 2021 07:34:20 +0100

Write in front

Is it still facing Baidu, Google and the process day after day? Have you ever thought about trying to use various design patterns in ordinary business? Think about a thousand roads at night and go the same way in the morning.
I have always despised the code written in the past, thinking about refactoring, but I dare not start. Many ancestral codes are written in a way that even I am afraid of myself, and there are even a few fragments that touch my whole body. Every time I see these codes, I wonder how to be elegant and elegant.
Last article "Reading Vue source code in one breath" I mentioned several times that the essence of higher-order functions is the function that returns another function internally. We can regard this practice as the feature that functions are first-class citizens.
Vue3 uses the composition API instead of the original option API, which makes me know that there is a paradigm called functional programming. In fact, functional programming is not uncommon in projects that use React, but because I haven't written a React project in more than three years, I have a little shallow knowledge. Even if you really know now, it's not that you suddenly become eager to learn and want to touch everything, but because of the recent online class. After all, you have thousands of ideas in your heart. If you really want to realize it, you have to rely on external forces.
If you want to have a complete and detailed understanding of functional programming, it is recommended JS functional programming guide , this article will not be more in place than this book. It is just some sorting, induction and summary of functional programming in all kinds of materials I read.
Finally, if you want to use the paradigm of functional programming to write code, it is recommended to use Lodash and Lodash/fp. The documents are complete. It is important to learn about the source code in your spare time. You will certainly have a deeper understanding of functional programming.

Functional programming

advantage

  • Make the code more concise and discard redundant junk code
  • Abandoning this in JavaScript solves the problem that the pointer does not know who to point to in daily work
  • Due to the characteristics of functional programming, tree shaping can work better in the packaging process
  • Modularity, small granularity, high reuse, low coupling, predictability, convenient for unit testing and debug ging

The function in functional programming does not refer to function, but the function in mathematics, that is, f(x)

The following function is a simple implementation of f(x) = x + 1

const plusOne = number => number + 1

Since it is the theory of mathematical category, it should meet the computational laws in the field of mathematics, such as exchange law, combination law and distribution law. The following example will show its wonderful use in functional programming

const plus = (x, y) => x + y
const multiply = (x, y) => x * y
// Commutative law
// 2 + 3 = 3 + 2 = 5
// 2 * 3 = 3 * 2 = 6
plus(2, 3) === plus(3, 2)
multiply(2, 3) === multiply(3, 2)
// Distribution rate
// 4 * (2 + 3) = (4 * 2) + (4 * 3) = 20
multiply(4, plus(2, 3)) === plus(multiply(4, 2), multiply(4, 3))
// Combination law
// (2 + 3) + 4 = 2 + (3 + 4) = 9
// (2 * 3) * 4 = 2 * (3 * 4) = 24
plus(plus(2, 3), 4) === plus(2, plus(3, 4))
multiply(multiply(2, 3), 4) === multiply(2, multiply(3, 4))

Function is a first-class citizen

  • Instead of treating functions as methods, they are used as ordinary objects
  • Since it is an ordinary object, it can be assigned to a variable, regarded as a return value, or stored in an array
    As the return value can be understood in the closure / Coriolis section later, it will not be introduced here, so only the assignment to variables will be introduced first. Since you want to assign a function to a variable, you need to use a function expression. What you can do here is to make the code more concise and discard redundant junk code, which I mentioned in the advantages
// Example 1
const hi = name => `Hi ${name}`
const greeting = name => hi(name)
const greeting = hi
// The greeting method passes a name into the hi method and calls the hi method
// In essence, the greeting method can be used directly, and the hi method will not be a problem
// So we can rewrite it to const greeting = hi

// Example 2
const getServerStuff = callback => ajaxCall(json => callback(json))
const getServerStuff = callback => ajaxCall(callback)
const getServerStuff = ajaxCall
// JSON = > callback (JSON) can be regarded as const f = JSON = > callback (JSON)
// Like the above example of const greeting = name = > hi (name), we can rewrite it as const f = callback
// Therefore, AjaxCall (JSON = > callback (JSON)) can be written as ajaxCall(f), that is, ajaxCall(callback)

// Callback = > AjaxCall (callback) can be regarded as const f = callback = > AjaxCall (callback)
// Similarly to the above, we can rewrite it as const f = ajaxCall
// Therefore, callback = > ajaxCall (callback) can be written as f, that is, ajaxCall
// Finally, const getServerStuff = ajaxCall is obtained

// Example 3
const BlogController = {
  index(posts) { return Views.index(posts) },
}
const BlogController = {
  index: posts => Views.index(posts),
}
const BlogController = {
  index: Views.index,
}

Higher order function

  • Functions can be used as arguments
  • Function can be used as the return value (see the coritization in the last section for details)
// Simple implementation of map method in Lodash
const _map = curry((f, target) => target.map(f))

This rewriting seems meaningless. It just encapsulates the native map method of the array?
At first glance, it is not. First_ The map method adjusts the position of methods and objects. For a function, the internal logic of the method is fixed, but the incoming variables are not the same. Therefore, it is not necessary to wait until all parameters are set before implementing this method. You can first pass in a method and then wait for the variable to be passed in. It is better to combine the combination function and Coriolis function below
Secondly, in the following chapters, I will introduce a container called functor. We need to call the map method of functor to operate the internal values of functor, so existence is reasonable

Pure function

Developers who have used Gulp or Webpack should have a certain impression of pipe and loader. The essence of its operation is to output a JavaScript executable function through the current pipe / loader method to the next pipe / loader method to process the code.


If the output results during the execution of pipe / loader are different every time (with side effects), it is impossible for us to have any confidence in the packaged results. Therefore, one of the characteristics of functional programming is pure function, and the fixed content input must be the fixed content

// Because slice has the same input and gets the same answer, slice is a pure function and splice is an impure function
const array = [1, 2, 3, 4, 5]
array.slice(0, 1) // [1]
array.slice(0, 1) // [1]
array.slice(0, 1) // [1]

array.splice(0, 1) // [1]
array.splice(0, 1) // [2]
array.splice(0, 1) // [3]

Side Effect

  • Since there are pure functions, there will be impure functions, that is, they have side effects
  • The internal reference of external parameters in the function body is regarded as a side effect
  • http requests in the function body are considered as side effects
  • Function body operation DOM
  • console.log is considered as a side effect
  • ...
    You may wonder about console Why is log a side effect? Please try to enter the following code in the browser
console.log = e => e + 1
console.log(2) // You get 3 instead of 2
// console is an externally controllable object, so it is impure when there are externally changeable objects in the function body

Cacheability Cacheable

Since the pure function will get the same output every time with the same input, once the function is complex, it is not necessary to run the whole process every time. You can directly get the output value of the input through the cache, that is, memoize technology

const memoize = f => {
  const cache = {}
  return function() {
    const input = JSON.stringify(arguments)
    cache[input] = cache[input] || f.call(this, ...arguments)
    return cache[input]
  }
}
const squareNumber = memoize(x => x * x)
squareNumber(4) // 16 calculated results
squareNumber(4) // 16 read the result with the input value of 4 from the cache

Portability / self documenting

The following function has_ The existence of toString seems to have side effects, but its call is actually in a closure, so it is delayed. It is still a pure function for the is method itself.


In addition, due to_ ToString is extracted from the function ontology and becomes a configurable function, so for the is method, it only needs to be changed_ The toString method can change its internal implementation_ ToString becomes the existence of modules, so as to achieve portability

const _toString = (target, type) => Object.prototype.toString.call(target) === `[object ${type}]`
const is = type => target => _toString(target, type)
const isObject = is('Object')
const isArray = is('Array')
const isFunction = is('Function')

Testability Testable

It is also because a fixed input will get a fixed output. During the test, it is not necessary to go through every process and finally arrive at the method to be tested. Just give an input to the method to be tested to test whether the result is what we want

Reasonable

As mentioned above, functions in mathematics have some laws, such as exchange law, combination law, etc. the rationality here is the same in mathematics, which means that if a piece of code can be replaced with the result of its execution, and it is replaced without changing the behavior of the whole program, then we say that this piece of code is transparent. For example, if (teamA.name === teamB.name) can be directly treated as if ('myTeam '= =' yourTeam ') in a pure function

Parallel code

Not a combined pure function can be parallel, because there is no data access between them.
But I have some questions about this. For example, the values of each object field in an array are often processed in daily business. Will it be more expensive to use parallel + functor?

Combination and coritization

The existence of combinatorial function is to realize the combination law in mathematics. Before looking at the following, please think about three problems, and then look at the contents of coritization and combinatorial function

Two questions:

    1. In the three methods call / apply / bind, why put the new pointer first in arguments?
    1. For data processing, do you think the data is confirmed first or the method?
    1. Why is the bind method called again after passing in the value? What is the implementation principle?

currying

  • Coriolism actually uses the idea of closure to store the arguments (mostly a method) of the parent function in the child function and execute it when the parameters are completed
  • The length of parameters may be known or unknown, so the implementation of coriolism needs to distinguish the number of parameters
  • When we only know one parameter and another parameter needs to be received in other ways, we can curry a function with multiple parameters, pass in a known value for the first call, and let the system call automatically after receiving the remaining values
// Coritization of known length
function curry(f) {
  // Get the number of parameters for f
  const len = f.length
  const inner = (...args) => {
    if (args.length === len) {
      return f.apply(this, args)
    } else {
      return function(...rest) {
        return inner(...args.concat(rest))
      }
    }
  }
  return inner
}
// Coriolis of unknown length - 1
function curry(f) {
  const params = []
  const inner = (...args) => {
    if (args.length) {
      params.push(...args)
      return inner
    } else {
      return f.apply(this, params)
    }
  }
  return inner
}
// Coriolis of unknown length - 2
function curry(f) {
   const inner = (...args) => {
    return function(...rest) {
      if (rest.length) {
        return inner(...args.concat(rest))
      } else {
        return f.apply(this, args)
      }
    }
  }
  return inner
}
const add = curry((a, b, c) => a + b + c)
console.log(add(1)(2)(3)()) // 6
console.log(add(1, 2, 3)()) // 6
console.log(add(1)(2, 3)()) // 6

Handwritten bind implementation

// Let's first understand the pointer problem of function call
const obj = {
  name: 'JavaScript',
  sayHello() {
    console.log(`hello ${this.name || 'nameless man'}`)
  }
}
const sayHello = obj.sayHello
obj.sayHello()
sayHello()
const aeorus = {
  name: 'aeorus'
}
const meetAeorus = obj.sayHello.bind(aeorus)
const meetAeorusAgain = sayHello.bind(aeorus)
meetAeorus()
meetAeorusAgain()
/*
If you know something about JavaScript calls, you should get the answer soon
 namely:
  hello JavaScript
  hello nameless man
  hello aeorus
  hello aeorus

In obj In fn (), obj is the caller, and the pointer inside fn points to obj
 In fn(), there is no visible caller, so the pointer inside fn points to window
 The bind method is to help fn reassign a pointer
*/
// Note: the arrow function cannot be used in this definition because the arrow function has no pointer
Function.prototype.myBind = function(...args) {
  const context = args[0] || window // Get environment context
  args = args.slice(1) // Get the parameters of fn
  const fn = this // Get fn because it was fn when called Bind, so this will point to fn
  context.fn = fn // Reassign fn's context
  return function() {
    return context.fn(...args) // Call fn in this context
  }
}

Combinatorial function

  • The operation mechanism of pipe and loader was mentioned in the previous section. A similar function, namely compose composite function, is also required in the method of functional programming idea we write
  • Combinatorial functions must conform to the combination law in mathematics, which means that no matter how the order of methods in the incoming method set changes, the answers must be the same, which is why one of the characteristics of functional programming is pure functions
const compose = (...args) => {
  // Arrange the method set in the composite function in reverse order
  args = args.reverse()
  // Use the reduce method to pass the result of the last execution to the next one as a parameter, and finally return the result
  return value => args.reduce((prev, curv) => curv(prev), value)
}
// Example 1
const toUpper = str => str.toUpperCase()
const reverse = array => array.reverse()
const first = array => array[0]
// Use impure console log 
const trace = curry((tag, result) => {
  console.log(`${tag} -> ${result}`)
  return result
})
const upperLastEle = compose(
  compose(
    toUpper,
    trace('after first'),
    first
  ),
  trace('after reverse'),
  reverse
)
// after reverse -> ['c', 'b', 'a']  after first -> c  completed -> C
trace('completed', upperLastEle(['a', 'b', 'c']))
// after reverse -> ['g', 'f', 'e']  after first -> g  completed -> G
trace('completed', upperLastEle(['e', 'f', 'g']))

// Example 2 optimization code
const authenticate = form => {
  const user = toUser(form)
  return logIn(user)
}
const authenticate = form => logIn(toUser(form))
const authenticate = compose(logIn, toUser)

closure

In fact, there is nothing to say about closures. If the above coritization content can be digested, you can actually say that you understand closures. But there are always some exceptions. For example, how does the garbage collection mechanism deal with closures? Why do you always hear closures cause memory leaks? In fact, the problem is how the garbage collection mechanism works when the variables existing in the parent function are referenced by the child function
Question 1: when the parent function is destroyed, will the variable be destroyed together? If not, why?
Question 2: when the sub functions are destroyed, will the variables be destroyed together? If not, why?
The answer is actually very simple. The parent function is destroyed immediately after calling, but because the variable is referenced by the child function, it will not be destroyed. When a sub function is destroyed immediately after calling, but because the variable is not a variable in its own scope, it has no right to mark it for destruction.

call chaining

I don't know if you have thought about the principle of chain call. If you don't understand it, I suggest you think about it first.


In fact, it is very simple in essence, that is, to return to a new self

class Calc {
  constructor(x) {
    this.__value = x
  }
  plus(x) {
    const result = x + this.__value
    return new Calc(result)
  }
  minus(x) {
    const result = this.__value - x
    return new Calc(result)
  }
}
new Calc(5).plus(3).minus(1).plus(2).minus(1) // Calc { __value: 8 }

New field

Finally, it's about letters. I personally find it difficult to understand. I keep writing demos, changing demos and thinking about application scenarios
Even if I made it up, I made it up. I didn't say much. There was some high energy ahead. I began to make it up.

Functor

  • A special container
  • The value inside the container can be of any type
  • Instead of directly manipulating values, a map method is exposed to handle the values in the container
    Like the Webpack plug-in, the function exposes a series of hooks, and then operates the compilation in the hook to modify the file. First, write a simple function
class Container {
  static of(x) {
    return new Container(x)
  }
  constructor(x) {
    this.__value = x
  }
  map(f) {
    return Container.of(f(this.__value))
  }
}
// Create a container named x to receive the value 9
const x = Container.of(9)
// Gets the value in the x container
console.log(x.__value) // 9
// Manipulate the value in the x container and get a new container named y
// This does not change the value in the x container
const y = x.map(value => value + 1)
// Gets the value in the y container
console.log(y.__value) // 10

Maybe functor

The above example is the simplest functor. The stored value takes the operating value, and everything seems to come naturally. But in the actual business, there are so many things that come naturally.
We can guess what will happen if NULL is passed in the functor? The answer is that an error will be reported, and an error will cause the function to become impure, which is something we don't want to see in functional programming. Therefore, a functor that can judge whether it is a null value was born out of nowhere - the Maybe functor

// When__ When value is not null
const input = Container.of(['a', 'b', 'c'])
const result = input.map(value => value.filter(item => item === 'a'))
console.log(result.__value) // ['a']
// When__ When value is null
const empty = Container.of()
const emptyResult = empty.map(value => value.filter(item => item === 'a')) // TypeError: Cannot read property 'filter' of undefined

Let's try to reprocess the above code using the Maybe functor

class Maybe extends Container {
  static of(x) {
    return new Maybe(x)
  }
  isNothing() {
    return this.__value === null || this.__value === undefined || !Object.keys(this.__value).length
  }
  map(f) {
    return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value))
  }
}
// When__ When value is not null
const input = Maybe.of(['a', 'b', 'c'])
const result = input.map(value => value.filter(item => item === 'a'))
console.log(result.__value) // ['a']
// When__ When value is null
const empty = Maybe.of()
const emptyResult = empty.map(value => value.filter(item => item === 'a'))
console.log(emptyResult.__value) // null

From the above example, we can find that the program does not report an error, which is really a gratifying thing. Next, we need to think about how to do something in combination with the business.
For example, e-commerce platforms always increase the number before the decimal point and decrease the number and unit after the decimal point.

const products = [{
  name: 'any-one',
  sellPrice: '9.9',
}, {
  name: 'aeo-rus',
  sellPrice: '29.9',
}, {
  name: 'no-price',
}, {
  sellPrice: '49.9',
}]
/* Expected output
[{
  float: "9",
  int: "9",
  name: "anyone",
  sellPrice: "9.9",
}, {
  float: "9",
  int: "29",
  name: "aeorus",
  sellPrice: "29.9",
}, {
  name: "noprice",
  sellPrice: null,
}, {
  float: "9",
  int: "49",
  name: null,
  sellPrice: "49.9",
}]
*/

How do we usually write when we are process oriented? Write a method to pass the field into format, and merge the returned value with the source data

// Non pointfree
const resolveProp = (f, key, target) => {
  const value = target[key] || null
  if (value) {
    const result = isObject(f(value)) ? f(value) : {
      [key]: f(value)
    }
    return Object.assign(target, result)
  } else {
    return Object.assign(target, {
      [key]: value
    })
  }
}
const formatName = name => name.split('-').join('')
const formatPrice = sellPrice => {
  const [int, float] = sellPrice.split('.')
  return {
    int,
    float,
  }
}
Object.assign(products,
  products.map(product => {
    resolveProp(formatName, 'name', product)
    resolveProp(formatPrice, 'sellPrice', product)
    return {
      ...product,
    }
  })
)

Now that we have learned the functor, we need to consider how to start. First, the API will pass the field according to whether there is a value, so we need to consider the null situation, so we have to choose the Maybe functor

// pointfree
const formatName = _map(name => name.split('-').join(''))
const formatPrice = _map(sellPrice => {
  const [int, float] = sellPrice.split('.')
  return {
    int,
    float,
  }
})
const resolveProduct = curry((target, f, key) => {
  const result = compose(
    _prop('__value'),
    compose(
      f,
      _map(_prop(key)),
    )
  )(target)
  return isObject(result) ? result : {
    [key]: result
  }
})
_map(product => {
  const resolveProp = resolveProduct(Maybe.of(product))
  Object.assign(product, {
    ...resolveProp(formatName, 'name'),
    ...resolveProp(formatPrice, 'sellPrice'),
  })
}, products)
  • I have finished writing, but I always feel that it is not too elegant. It seems that the amount of code has not been reduced and the difficulty of understanding has increased. Don't worry, then look back. After all, how can there be only one letter in business?
  • Incidentally, a may auxiliary function is often used in the May functor to dynamically modify the return value to achieve the effect of error collection or specifying the return value
// Use the May helper function to assign an undefined field to null and return
const maybe = curry((x, f, m) => m.isNothing() ? x : f(m))
const resolveProduct = curry((target, f, key) => {
  const result = compose(
    maybe(null, prop('__value')),
    compose(
      f,
      _map(prop(key)),
    )
  )(target)
  return isObject(result) ? result : {
    [key]: result
  }
})

I mentioned something less elegant above, such as resolveProduct. It seems that there is nothing wrong with this method, but we need to think about the purpose of the letter? Is it just to provide a map method for_ Is the map method invoked to the internal value in the combinatorial function? It doesn't seem to solve the defects caused by the onion model of combinatorial function, so it seems that the function of functor is not big? Let's try rewriting the resolveProduct method

const resolveProduct = curry((target, f, key) => {
  const sourceValue = target.map(_prop(key))
  const formatValue = maybe(Maybe.of(null), f, sourceValue)
  const result = formatValue.map(value => isObject(value) ? value : {
    [key]: value
  })
  return result.__value
})

It feels more beautiful, but it's not enough

Either functor

  • The maybe functor can handle that when the internal value is null, it does not call the function passed in the map method. It can even achieve the dynamic control of the return value of the final result of chain call through the maybe auxiliary function. But if we want the first parameter of the maybe auxiliary function to also call the function passed in the map method? Although we can rewrite the May auxiliary function, it is not reasonable
  • Theoretically, try... Catch... Is not pure, because catch may throw all kinds of exceptions and terminate the program. In daily development, we want the program to report an error, but we don't want the program to terminate. If possible, it's best to collect the error information through an errors object, and then uniformly send it to the background through new Image for logging
    Don't say much. Let's write an Either letter first
class Either {
  static of(left, right) {
    return new Either(left, right)
  }
  constructor(left, right) {
    this.left = left
    this.right = right
  }
  map(f) {
    return this.right ?
      Either.of(this.left, f(this.right)) :
      Either.of(this.right, f(this.left))
  }
}

Next, let's look at some things we can do in combination with the business


For example, the e-commerce platform will classify provinces and cities according to the geographical location of the chain platform. As mentioned above, in the back-end architecture, the field may not be returned when the merchant does not fill in a value, so we need to use the Maybe functor to wrap the source data

const userInfo = {
  address: 'Jiangning District, Nanjing City, Jiangsu Province'
}
/* Expected output
[
  'Jiangning District, Nanjing City, Jiangsu Province,
  'Jiangsu Province ',
  'Nanjing city ',
  'Jiangning District ',
  undefined,
  '',
  index: 0,
  input: 'Jiangning District, Nanjing City, Jiangsu Province,
  groups: [Object: null prototype] {
    province: 'Jiangsu Province ',
    city: 'Nanjing city ',
    county: 'Jiangning District ',
    town: undefined,
    village: ''
  }
]
*/
const userInfo = null || {}
/* Expected output
[
  'Jingkou District, Zhenjiang City, Jiangsu Province,
  'Jiangsu Province ',
  'Zhenjiang City ',
  'Jingkou District ',
  undefined,
  '',
  index: 0,
  input: 'Jingkou District, Zhenjiang City, Jiangsu Province,
  groups: [Object: null prototype] {
    province: 'Jiangsu Province ',
    city: 'Zhenjiang City ',
    county: 'Jingkou District ',
    town: undefined,
    village: ''
  }
]
*/
const regex = "(?<province>[^province]+province|.+Autonomous Region)(?<city>[^autonomous prefecture]+autonomous prefecture|[^city]+city|[^Alliance]+Alliance|[^region]+region|.+Zoning)(?<county>[^city]+city|[^county]+county|[^flag]+flag|.+area)?(?<town>[^area]+area|.+town)?(?<village>.*)"
const match = curry((regex, target) => target.match(regex))
const adddress = Maybe.of(userInfo).isNothing() || Maybe.of(userInfo).map(_prop('address')).isNothing() ?
  Either.of('Jingkou District, Zhenjiang City, Jiangsu Province', null) :
  Either.of(null, _prop('address', userInfo))
const result = adddress.map(match(regex))

After this code is written, let's not say whether it's elegant or not. First, it's full of force
Next, let's look at the application of Either functor in error collection

const parseJSON = (str) => {
  try {
    return Either.of(null, JSON.parse(str))
  } catch(e) {
    return Either.of({ error: e.message }, null)
  }
}
// Either { left: { error: 'Unexpected token n in JSON at position 2' }, null }
parseJSON('{ name: aeorus }')
// Either { left: null, right: { name: 'aeorus' } }
parseJSON('{ "name": "aeorus" }')

ap functor

  • The abbreviation of applied, which aims to operate one functor on another
  • The internal value of A functor is A constant, and the internal value of B functor is A function. When you want to use the internal function of B functor to operate the internal value of A functor, you can use ap functor
class Ap extends Container {
  static of(x) {
    return new Ap(x)
  }
  ap(functor) {
    return Ap.of(this.__value(functor.__value))
  }
  map(f) {
    return Ap.of(f(this.__value))
  }
}

This letter puzzled me for a long time. I've been thinking about it_ Isn't the map method enough? Where is the practical application scenario of this functor? First write a simple application function for everyone to understand

// Input 2 3
// Expected output 5
const plus = curry((x, y) => x + y)
console.log(
  _prop(
    '__value',
    Ap.of(plus)
      .ap(Container.of(2))
      .ap(Container.of(3))
  )
)
console.log(
  _prop(
    '__value',
    Ap.of(plus(2))
      .ap(Container.of(3))
  )
)
console.log(
  _prop(
    '__value',
    Ap.of(2)
      .map(plus)
      .ap(Container.of(3))
  )
)

After writing, do you think it's a little routine to pass in methods in map and values in ap?
Next, rewrite it into ap functor in combination with the business case provided in Maybe functor

const formatName = _map(name => name.split('-').join(''))
const formatPrice = _map(sellPrice => {
  const [int, float] = sellPrice.split('.')
  return {
    int,
    float,
  }
})
const resolveProduct = curry((target, name, sellPrice) => {
  Object.assign(target, {
    name,
    ...sellPrice
  })
})
const resolveProp = (f, prop) => maybe(Maybe.of(null), f, prop)
_map(product => {
  Ap.of(resolveProduct(product))
    .ap(resolveProp(formatName, Maybe.of(product.name)))
    .ap(resolveProp(formatPrice, Maybe.of(product.sellPrice)))
}, products)

Once said, do you feel that the force grid suddenly rises? What happened just now has become a lot more elegant?

IO functor

  • Most of the time, we can't be a complete pure function. We will call or access some external methods or values inside the function. We used to delay the execution of this side effect by Coriolis, but this practice is not very good. After all, no one is an ostrich. If his head is buried in the sand, he can't see it?
  • Let's take business as an example. For example, when there are too many commodities and there are classifications, we often cache the data in the global Map to save bandwidth. When we switch to that classification, we directly obtain the data corresponding to the uid of the classification from the Map. If there is no classification, we will request it again

In the following case, the getProductsByUid method is obtained when getProductsByCache is called for the first time. The specific products of this category can be obtained through the getProductsByUid method

const productsMap = {
  '3211011': [{
    name: 'any-one',
    sellPrice: '9.9',
  }, {
    name: 'aeo-rus',
    sellPrice: '29.9',
  }]
}
/* Expected output of ordinary pure function writing
{
  '3211011': [{
    name: 'any-one',
    sellPrice: '9.9',
  }, {
    name: 'aeo-rus',
    sellPrice: '29.9',
  }],
  '3211012': [{
    name: 'anyone',
    sellPrice: '39.9',
  }, {
    name: 'aeorus',
    sellPrice: '49.9',
  }]
}
*/
/* IO Functor expected output
{
  '3211011': [
    { name: 'anyone', sellPrice: '9.9', int: '9', float: '9' },
    { name: 'aeorus', sellPrice: '29.9', int: '29', float: '9' }
  ],
  '3211012': [
    { name: 'anyone', sellPrice: '39.9', int: '39', float: '9' },
    { name: 'aeorus', sellPrice: '49.9', int: '49', float: '9' }
  ]
}
*/
const getProductsByRequest = async uid => {
  try {
    let result
    if (!Object.prototype.hasOwnProperty.call(productsMap, uid)) {
      await Promise.resolve([{
        name: 'anyone',
        sellPrice: '39.9',
      }, {
        name: 'aeorus',
        sellPrice: '49.9',
      }]).then(products => {
        result = productsMap[uid] = products
      })
    } else {
      result = productsMap[uid]
    }
    return result
  } catch (e) {
    return Promise.reject()
  }
}
const getProductsByCache = () => {
  const getProductsByUid = async uid => {
    const result = await getProductsByRequest(uid)
    return result
  }
  return getProductsByUid
}
const productRequest = getProductsByCache()
productRequest(3211011).then(products => {
  console.log(products)
})
productRequest(3211012).then(products => {
  console.log(products)
})
setTimeout(() => {
  productRequest(3211012).then(products => {
    console.log(products)
  })
}, 1000)
  • In IO functor__ value is a function
  • IO functors can store impure operations in__ value, delay the execution of this impure operation
  • Leave impure operations to the caller
class IO {
  static of(f) {
    return new IO(() => f)
  }
  constructor(f) {
    this.__value = f
  }
  map(f) {
    return new IO(compose(f, this.__value))
  }
}

Next, rewrite the common writing method of the above case in combination with the business logic of the case provided in the ap functor

const resolveProduct = curry((target, name, sellPrice) => {
  Object.assign(target, {
    name,
    ...sellPrice
  })
})
const resolveProp = (f, prop) => maybe(Maybe.of(null), f, prop)
const getProductsByRequest = IO.of(async uid => {
  try {
    let result
    if (!Object.prototype.hasOwnProperty.call(productsMap, uid)) {
      await Promise.resolve([{
        name: 'anyone',
        sellPrice: '39.9',
      }, {
        name: 'aeorus',
        sellPrice: '49.9',
      }]).then(products => {
        result = productsMap[uid] = products
      })
    } else {
      result = productsMap[uid]
    }
    return result
  } catch (e) {
    return Promise.reject()
  }
})
const getProducts = uid => _map(
  compose(
    _then(
      _map(product => {
        Ap.of(resolveProduct(product))
          .ap(resolveProp(formatName, Maybe.of(product.name)))
          .ap(resolveProp(formatPrice, Maybe.of(product.sellPrice)))
        // trace(productsMap)
      })
    ),
    _apply([uid])
  ),
  getProductsByRequest
)
getProducts(3211011).__value()
getProducts(3211012).__value()
setTimeout(() => {
  getProducts(3211012).__value()
}, 1000)

The function of IO functor lies in what information is not needed. I finish writing the method first, which guarantees that I am absolutely pure. As for the last call, it is pure, that is your pot. Anyway, I am not sticky.

demo and utils

The demo and methods quoted in the article will be added below. You can download them and measure them one by one. I think I've made it up quite well

Container.js

export class Container {
  static of(x) {
    return new Container(x)
  }
  constructor(x) {
    this.__value = x
  }
  map(f) {
    return Container.of(f(this.__value))
  }
}

Maybe.js

import { Container } from './container.js'
import { curry, _prop, _map, isObject } from './utils.js'

export class Maybe extends Container {
  static of(x) {
    return new Maybe(x)
  }
  isNothing() {
    return this.__value === null || this.__value === undefined || !Object.keys(this.__value).length
  }
  map(f) {
    return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value))
  }
}
export const maybe = curry((x, f, m) => m.isNothing() ? x : f(m))

const products = [{
  name: 'any-one',
  sellPrice: '9.9',
}, {
  name: 'aeo-rus',
  sellPrice: '29.9',
}, {
  name: 'no-price',
}, {
  sellPrice: '49.9',
}]
const formatName = _map(name => name.split('-').join(''))
const formatPrice = _map(sellPrice => {
  const [int, float] = sellPrice.split('.')
  return {
    int,
    float,
  }
})
const resolveProduct = curry((target, f, key) => {
  const sourceValue = target.map(_prop(key))
  const formatValue = maybe(Maybe.of(null), f, sourceValue)
  const result = formatValue.map(value => isObject(value) ? value : {
    [key]: value
  })
  return result.__value
})
_map(product => {
  const resolveProp = resolveProduct(Maybe.of(product))
  Object.assign(product, {
    ...resolveProp(formatName, 'name'),
    ...resolveProp(formatPrice, 'sellPrice'),
  })
}, products)

Either.js

import { Maybe } from './Maybe.js'
import { curry, _prop, _map } from './utils.js'

class Either {
  static of(left, right) {
    return new Either(left, right)
  }
  constructor(left, right) {
    this.left = left
    this.right = right
  }
  map(f) {
    return this.right ?
      Either.of(this.left, f(this.right)) :
      Either.of(this.right, f(this.left))
  }
}

const userInfo = {
  address: 'Jiangning District, Nanjing City, Jiangsu Province'
}
// const userInfo = {
//   name: 'aeorus'
// }
// const userInfo = null
const regex = "(?<province>[^province]+province|.+Autonomous Region)(?<city>[^autonomous prefecture]+autonomous prefecture|[^city]+city|[^Alliance]+Alliance|[^region]+region|.+Zoning)(?<county>[^city]+city|[^county]+county|[^flag]+flag|.+area)?(?<town>[^area]+area|.+town)?(?<village>.*)"
const match = curry((regex, target) => target.match(regex))
const adddress = Maybe.of(userInfo).isNothing() || Maybe.of(userInfo).map(_prop('address')).isNothing() ?
  Either.of('Jingkou District, Zhenjiang City, Jiangsu Province', null) :
  Either.of(null, _prop('address', userInfo))
adddress.map(match(regex))

const parseJSON = (str) => {
  try {
    return Either.of(null, JSON.parse(str))
  } catch (e) {
    return Either.of({ error: e.message }, null)
  }
}
// Either { left: { error: 'Unexpected token n in JSON at position 2' }, null }
parseJSON('{ name: aeorus }')
// Either { left: null, right: { name: 'aeorus' } }
parseJSON('{ "name": "aeorus" }')

ap.js

import { Container } from "./Container.js"
import { Maybe, maybe } from "./Maybe.js"
import { _prop, _map, curry } from "./utils.js"

export class Ap extends Container {
  static of(x) {
    return new Ap(x)
  }
  ap(functor) {
    return Ap.of(this.__value(functor.__value))
  }
  map(f) {
    return Ap.of(f(this.__value))
  }
}

const products = [{
  name: 'any-one',
  sellPrice: '9.9',
}, {
  name: 'aeo-rus',
  sellPrice: '29.9',
}, {
  name: 'no-price',
}, {
  sellPrice: '49.9',
}]
const formatName = _map(name => name.split('-').join(''))
const formatPrice = _map(sellPrice => {
  const [int, float] = sellPrice.split('.')
  return {
    int,
    float,
  }
})
const resolveProduct = curry((target, name, sellPrice) => {
  Object.assign(target, {
    name,
    ...sellPrice
  })
})
const resolveProp = (f, prop) => maybe(Maybe.of(null), f, prop)
_map(product => {
  Ap.of(resolveProduct(product))
    .ap(resolveProp(formatName, Maybe.of(product.name)))
    .ap(resolveProp(formatPrice, Maybe.of(product.sellPrice)))
}, products)

IO.js

import { curry, compose, _map, _apply, _then } from './utils.js'
import { Maybe, maybe } from "./Maybe.js"
import { Ap } from './ap.js'

class IO {
  static of(f) {
    return new IO(() => f)
  }
  constructor(f) {
    this.__value = f
  }
  map(f) {
    return new IO(compose(f, this.__value))
  }
}

const productsMap = {
  '3211011': [{
    name: 'any-one',
    sellPrice: '9.9',
  }, {
    name: 'aeo-rus',
    sellPrice: '29.9',
  }]
}
const formatName = _map(name => name.split('-').join(''))
const formatPrice = _map(sellPrice => {
  const [int, float] = sellPrice.split('.')
  return {
    int,
    float,
  }
})
const resolveProduct = curry((target, name, sellPrice) => {
  Object.assign(target, {
    name,
    ...sellPrice
  })
})
const resolveProp = (f, prop) => maybe(Maybe.of(null), f, prop)
const getProductsByRequest = IO.of(async uid => {
  try {
    let result
    if (!Object.prototype.hasOwnProperty.call(productsMap, uid)) {
      await Promise.resolve([{
        name: 'anyone',
        sellPrice: '39.9',
      }, {
        name: 'aeorus',
        sellPrice: '49.9',
      }]).then(products => {
        result = productsMap[uid] = products
      })
    } else {
      result = productsMap[uid]
    }
    return result
  } catch (e) {
    return Promise.reject()
  }
})
const getProducts = uid => _map(
  compose(
    _then(
      _map(product => {
        Ap.of(resolveProduct(product))
          .ap(resolveProp(formatName, Maybe.of(product.name)))
          .ap(resolveProp(formatPrice, Maybe.of(product.sellPrice)))
      })
    ),
    _apply([uid])
  ),
  getProductsByRequest
)
getProducts(3211011).__value()
getProducts(3211012).__value()
setTimeout(() => {
  getProducts(3211012).__value()
}, 1000)

utils.js

export const curry = f => {
  return function inner(...args) {
    if (f.length === args.length) {
      return f.apply(this, args)
    } else {
      return function (...rest) {
        return inner(...args.concat(rest))
      }
    }
  }
}
const is = type => target => Object.prototype.toString.call(target) === `[object ${type}]`
export const isObject = is('Object')
export const isArray = is('Array')
export const isFunction = is('Function')
export const compose = (...args) => {
  args = args.reverse()
  return value => args.reduce((prev, curv) => curv(prev), value)
}
export const _prop = curry((key, target) => target[key])
export const _map = curry((f, target) => target.map(f))
export const trace = x => {
  console.log(x)
  return x
}
export const _filter = curry((f, target) => target.filter(f))
export const _split = curry((regex, target) => target.split(regex))
export const _apply = curry((args, target) => target.apply(this, args))
export const _then = curry((f, target) => target.then(f))

Write at the end

So far, functional programming has been finished, especially functors. Although there are Pointed functors / Task functors / Monad functors, these are enough for a pot.
Functional programming took about three days, and letters took two and a half days. It took two and a quarter days from completely confused application scenarios to suddenly realizing that "it should be used like this". I don't know if my Epiphany is a real epiphany. Maybe my so-called right application scenario is still wrong. If the watchers have unique opinions, please help point out and learn together.
That's it. I'll see you in the next chapter.

Topics: Front-end Functional Programming