What is functional programming

We often hear about functional programming in development or in interviews, but what is it? Why should we learn functional programming? Don’t worry, let’s take a look at some code:

    // Non-functional programming
    let num1 = 2
    let num2 = 3
    let sum = num1 + num2
    console.log(sum)
    
    // Functional programming
    function add (n1, n2) {
    return n1 + n2
    }
    let sum = add(2.3)
    console.log(sum)
Copy the code

The above code is very simple, it is the sum of two numbers, in our process-oriented way of thinking, we define two variables to receive these two values, the end result is also defined variables to receive. At first glance, it looks pretty straightforward, but one of the problems is that we’re only dealing with the sum of 2 plus 3, if we’re dealing with 5 plus 8; 7 + 9; Or more? If we think about it in a process-oriented way that we have to define more variables, that’s a lot of code, so we have to introduce functional programming.

Functional Programming is a Programming paradigm, a style of software development. Now, let’s look at the sum of two numbers that we did in functional programming, where we took the sum out and encapsulated it into a function. In the future, when we call, we don’t have to care about the implementation process, just need to focus on our function call, which can be said to greatly simplify our code. This is the core idea of functional programming: abstract the process, focus only on the result of the operation, and not so much on the implementation of the operation.

By comparing the two methods of programming, we can also see the advantages of functional programming. Let me outline a few points:

  • Functional programming is getting more attention with the popularity of React.
  • Vue 3 also began to embrace functional programming.
  • Functional programming can discard this.
  • Better use of Tree Shaking to filter out useless code during packaging.
  • Convenient for testing and parallel processing.
  • There are many libraries to help us with functional development: Lodash, underscore, Ramda.

Common concepts in functional programming

1. 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.

We can call the functions that satisfy the above two points as higher-order functions. The common methods of JS array, such as Map, filter, find, reduce, forEach, etc., are all higher-order functions. Higher-order functions are used to abstract general problems, while abstraction can help us shield details and only need to focus on our goals. Common higher-order functions include forEach, map, filter, every, some, etc.

// Process oriented approach
let array = [1.2.3.4]
for (let i = 0; i < array.length; i++) {
console.log(array[i])
}

// Higher-order higher-order functions
// We can see that higher-order functions help us mask details
let array = [1.2.3.4]
forEach(array, item= > {
console.log(item)
})
Copy the code

2. The closure

  • 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.

A closure must be a function object. Simply put, a closure is a function that reads variables inside other functions. Closures are useful for two things: they allow you to read variables inside a function outside of it; Keep the values of these variables in memory at all times. However, the same closures cause variables in functions to be stored in memory, which increases memory consumption. Do not abuse closures, which can cause performance problems for web pages and memory leaks in earlier versions of IE.

function add(){
    var n = 5;
    // Return a function inside the function and access its internal member variable n, creating a closure
    return function fn2() {
        n++;
        returnn; }}var fn = add();
    console.log( fn() );/ / 6
    console.log( fn() );/ / 7
    console.log( fn() ); / / 8
Copy the code

3. Pure functions

  • The same input will always get the same output, without any observable side effects.
  • A pure function is like a function in mathematics, y = f(x).

Let’s start with a piece of code:

/ / pure functions
function getArea (r) {
  console.log(r)// This will be printed three times, remember that we will modify this later to make a cache, which is also the advantage of pure functions
  return Math.PI * r * r
}
console.log(getArea(4))/ / 50.26548245743669
console.log(getArea(4))/ / 50.26548245743669
console.log(getArea(4))/ / 50.26548245743669
// impure function
let numbers = [1.2.3.4.5]
numbers.splice(0.3)
// => [1, 2, 3]
numbers.splice(0.3)
/ / = > [4, 5]
numbers.splice(0.3)
/ / = > []
Copy the code

It can be seen that for pure functions, the same input must have the same output. Therefore, when pure functions are called multiple times, they can be cached to improve performance. Let’s modify the above getArea function to make a cache. See how pure functions can cache to improve performance.

    // Implement a caching method first
function memoize(f) {
  let cache = {}
  return function () {
    let key = JSON.stringify(arguments)
    cache[key] = cache[key] || f.apply(f, arguments)
    return cache[key]
  }
}

function getArea(r) {
  console.log(r) // This will now only be executed once
  return Math.PI * r * r
}

let getAreaWithMemory = memoize(getArea)
console.log(getAreaWithMemory(4))/ / 50.26548245743669
console.log(getAreaWithMemory(4))/ / 50.26548245743669
console.log(getAreaWithMemory(4))/ / 50.26548245743669
Copy the code

We can also see from the above example that the pure function itself does not have the function of caching, just because we know that the pure function always has the same result for the same input, so we can cache the result of the pure function, the next time for the same input, directly give the result, do not need to calculate. Let’s see what the side effects of pure functions mean:

/ / not pure
let mini = 18
function checkAge1 (age) {
return age >= mini
}
// Pure (hardcoded, later can be solved by Currization)
function checkAge2 (age) {
let mini = 18
return age >= mini
}
Copy the code

The checkAge1 variable is the same as the checkAge1 variable, and the checkAge1 variable is the same as the checkAge1 variable. If the function depends on the external state, it cannot guarantee the same output, which can have side effects. Common sources of side effects include configuration files, databases, and retrieving user input. So side effects are impossible to avoid, because the code will inevitably depend on external configuration files, databases, etc., only to the extent that they can be controlled.

4. Mr Currie

  • When a function has more than one argument, it is called by passing some of the arguments (these arguments never change).
  • It then returns a new function that takes the remaining arguments and returns the result.

Next we solve the hard coding problem from the previous section:

function checkAge (age) {
let min = 18
return age >= min
}
// plain pure function
function checkAge (min, age) {
return age >= min
}
checkAge(18.24)
checkAge(18.20)
checkAge(20.30)
/ / curry
function checkAge (min) {
return function (age) {
return age >= min
}
}
let checkAge18 = checkAge(18)
let checkAge20 = checkAge(20)
checkAge18(
checkAge18(20)
Copy the code

You can see that corrification allows us to pass in fewer arguments to a function and get a new function that has some fixed arguments memorized. This is actually a ‘cache’ of function parameters, making functions more flexible and smaller in granularity. We can even convert multivariate functions into unary functions, and combine functions to produce powerful functions. Now let’s implement a Coriolization function ourselves.

function curry (func) {
// A currified function takes a function as an argument and passes out a currified function to be called
  return function curriedFn(. args) {
    // Determine the number of arguments and parameters
    if (args.length < func.length) {
      return function () {
      If the number of arguments does not match the number of parameters, call it recursively until
      // Arguments correspond to parameters 1, and entries like curried(1)(2, 3) are printed below
    // Flatten it to curried(1, 2, 3), and call func
        returncurriedFn(... args.concat(Array.from(arguments)))}}returnfunc(... args) } }function getSum (a, b, c) {
  return a + b + c
}

const curried = curry(getSum)

console.log(curried(1.2.3))
console.log(curried(1) (2.3))
console.log(curried(1.2) (3))
Copy the code

5. Function combination

  • If a function needs to be processed by multiple functions to get the final value, it is possible to combine the intermediate functions into a single function.
  • A function is like a pipeline of data, and a combination of functions connects these pipes, allowing data to pass through multiple pipes to form the final result.
  • Function combinations are executed from right to left by default.

It’s easy to write the onion code h(g(f(x))). For example, if we want to get the last element of an array and convert it to uppercase, we would think of _.toupper (.first(.reverse(array))). Code is layered on top of code. This example is still simple, easy to understand, and easy to see the logical relationship, but in real development, the scenario is undoubtedly much more complex, and if the code is written like this, it is more painful to maintain. So let’s see how we can solve this problem more elegantly with a combination of functions.

// combine functions
function compose (. fns) {
return function (value) {
return fns.reverse().reduce(function (acc, fn) {
return fn(acc)
}, value)
}
}
function first (arr) {
return arr[0]}function reverse (arr) {
return arr.reverse()
}
// Run from right to left
let last = compose(first, reverse)
console.log(last([1.2.3.4]))

Copy the code

How about, the code looks a lot cleaner and easier to read, although I wrote a bunch of code above, but in real development, we might use the “let last = compose(first, reverse)” sentence, which is simpler and easier to understand and maintain than the onion code.

6. Functor

  • Functor: a special container implemented by an ordinary object that has a map method that runs a function to manipulate values (deformation relationships).
  • Container: contains values and their deformation relationships (the deformation relationships are functions).

So far we’ve covered some of the basics of functional programming, but we haven’t shown you how to keep side effects under control, exception handling, asynchronous operations, and so on in functional programming. Functors can handle these problems. Let’s look at the implementation of functors:

Functor 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)

// But if the value is accidentally passed a null value (side effect), this time will be an error, so we introduce the MayBe functor
Container.of(null)
.map(x= > x.toUpperCase())
// TypeError: Cannot read property 'toUpperCase' of null
Copy the code

MayBe functor

  • We may encounter many errors in the process of programming, and we need to deal with these errors accordingly.
  • The MayBe functor is used to deal with external null cases (to keep side effects within permissible limits).
/ / MayBe functor
class MayBe {
  static of (value) {
    return new MayBe(value)
  }

  constructor (value) {
    this._value = value
  }

  map (fn) {
    return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
  }

  isNothing () {
    return this._value === null || this._value === undefined}}// Pass in a specific value
MayBe.of('Hello World')
.map(x= > x.toUpperCase())
// When null is passed
MayBe.of(null)
.map(x= > x.toUpperCase())
// => MayBe { _value: null }

Copy the code

Either functor

  • Either of the two, similar to if… else… Processing.
  • Exceptions make functions impure, and Either functors can be used for exception handling.
/ / Either functor
class Left {
  static of (value) {
    return new Left(value)
  }

  constructor (value) {
    this._value = value
  }

  map (fn) {
    return this}}class Right {
  static of (value) {
    return new Right(value)
  }

  constructor (value) {
    this._value = value
  }

  map (fn) {
    return Right.of(fn(this._value))
  }
}
function parseJSON (str) {
  try {
    return Right.of(JSON.parse(str))
  } catch (e) {
    return Left.of({ error: e.message })
  }
}
/ / normal
let r = parseJSON('{ "name": "zs" }')
          .map(x= > x.name.toUpperCase())
console.log(r)
// Abnormal value
let r1 = parseJSON('{ name: zs }')
console.log(r1)
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 execution of the impure operation (lazy execution), and wrap the current operation.
  • Leave impure operations to the caller.

Task functor

  • Can be used for asynchronous operations such as reading file information.
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))
}
}
let io = IO.of(process).map(p= > p.execPath)
console.log(io._value())
Copy the code
// Task Processes asynchronous tasks
const fs = require('fs')
const { task } = require('folktale/concurrency/task')
const { split, find } = require('lodash/fp')

function readFile (filename) {
  return task(resolver= > {
    fs.readFile(filename, 'utf-8'.(err, data) = > {
      if (err) resolver.reject(err)

      resolver.resolve(data)
    })
  })
}

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

Monad functor

  • A Monad functor flattens a functor that returns a functor.
  • The value encapsulated inside a Monad functor is a function (which returns a functor) that is intended to avoid functor nesting through the join method.
const fp = require('lodash/fp')
// IO Monad
class IO {
static of (x) {
return new IO(function () {
return x
})
}
constructor (fn) {
this._value = fn
}
map (fn) {
return new IO(fp.flowRight(fn, this._value))
}
join () {
return this._value()
}
flatMap (fn) {
return this.map(fn).join()
}
}
let r = readFile('package.json')
.map(fp.toUpper)
.flatMap(print)
.join()

Copy the code