JavaScript functional programming

JavaScript functional programming

Lead

This article is a reading note, the content of the article is "JavaScriopt ES6 Functional Programming Introduction Classic"

Introduction to functional programming

What is functional programming and why is it important

Functions in mathematics

f(x) = y
// f x y
 

key point:

  • Function must always accept one parameter
  • Function must always return a value
  • The function should be run based on the received parameters, not the external environment
  • For a given x, only one y will be output

Referential transparency

All functions return the same value for the same input, this property function is called referential transparency (Referential Transparency)

// identity 
var identity = (i) => {return i}
 

Replacement model

Use a referentially transparent function between other function calls.

sum(4,5) + identity(1)
 

According to the definition of referential transparency, we can replace the above statement with:

sum(4,5) + 1
 

This process is called Subsitution Model, because the logic of the function does not depend on other global variables, you can directly replace the result of the function, which is the same as her value. So this makes concurrent code and caching possible

Concurrent code: When running concurrently, if you rely on global data, to ensure that the data is consistent, it must be synchronized, and a lock mechanism is required when necessary. Functions that follow referential transparency only rely on the input of parameters, so they can run freely.

Caching: Since the function returns the same value for a given input, we can actually cache it. For example, if we implement a function that calculates the factorial of a given value, we can cache the result of each factorial, and use it next time without calculation. For example, the first input of 5, the result is 120. The second input of 5, we know that the result must be 120, so we can return the cached value without having to calculate it again.

Command, declarative and abstract

Functional programming advocates declarative functions and writing abstract code.

Imperative and declarative comparison

   // 

   /* */
    var array = [1,2,3]
    for(var i = 0; i < array.length; i++)
    console(array[i])//  1,2,3
    
   // 
   // 


   /* */
    var array = [1,2,3]
    array.forEach((element) => console.log(element))//  1,2,3
    
   // 
 

Functional programming advocates creating functions in an abstract way , such as forEach above, and these functions can be reused in other parts of the code.

Pure function

A pure function is a function that returns the same output for a given input, and the pure function should not rely on any external variables, nor should it change any external variables.

  • High concurrency: Pure functions always allow us to execute code concurrently, because pure functions will not change his environment
  • Cacheable: Since pure functions always return the same output for a given input, we can cache the output of the function.

Higher order function

A function that accepts another function as its parameter or a function that returns a function is called a higher-order function (Higher-OrderFunction)

javaScript data type

  • Undefined
  • BigInt
  • String
  • Null
  • Number
  • Boolean
  • Object

Function is a data type of JavaScript. Since the function is a data type similar to String, we can store the function in a variable and pass it as a parameter of the function. So the function in JavaScript is a first-class citizen. When a language allows functions When used as any other data type, a function is called a first-class citizen. That is to say, a function can be assigned to a variable, passed as a parameter, or returned by other functions.

Abstract and higher-order functions

Generally speaking, higher-order functions are usually used for general problems. In other words, higher-order functions are defined abstractions.

Abstraction: In software engineering and computer science, abstraction is a technique for managing the complexity of computer systems. By establishing the complexity of the interaction between a person and the system, the more complex details are suppressed below the current level. In short, abstraction allows us to focus on the predetermined goal without worrying about the underlying system concept.

// forEach 
const forEach = (array,fn) => {
  let i;
  for(i=0;i<array.length;i++) {
    fn(array[i])
  }
}

// forEach 

let array = [1,2,3]
forEach(array,(data) => console.log(data)) 

 

Closures and higher-order functions

A closure is an internal function. What is an internal function? It is a function inside another function.

The power of closure lies in its access to the scope chain (or scope level)

3.accessible scopes of closures

  • Variables declared within its own declaration
  • Access to global variables
  • Access to external function variables ( emphasis )
//
let globalStr = 'global';
function outer(){
//
   let outerStr = 'outer'
   function inter(){
   //
       let interStr = 'inter'
       console.log(globalStr)
       console.log(outerStr)
       console.log(interStr)
   }
   
   inter();
}

outer();

 

Remember the location of the closure

An important concept in closures-closures can remember its context!!

//
let globalStr = 'global';
function outer(){
//
   let outerStr = 'outer'
   function inter(){
   //
       let interStr = 'inter'
       console.log(globalStr)
       console.log(outerStr)
       console.log(interStr)
   }
   
   return inter
}

let inter = outer();
inter()

 

Implement a tab function

The tab function accepts a value and returns a closure function of valued, the function will be executed

    
    
let tap = (value)=>
  (fn)=> (
    typeof(fn) === 'function' && fn(value),
    console.log(value)
  )

 
 tap('func')((it)=>console.log('value of '+it))
 
 //
 value of func
 func
 
 

Implement a reduce function

    
const reduce = (array,fn,initialValue)=>{
  let accumlator;

  if(initialValue != undefined)
    accumlator = initialValue;
  else
    accumlator = array[0];

  if(initialValue == undefined)
    for(let i = 1;i<array.length;i++)
      accumlator = fn(accumlator,array[i])
  else 
    for(const value of array)
      accumlator = fn(accumlator,value)

  return [accumlator]
}

let total = reduce([1,2,3,4],(acc,val)=>acc*val,1)

console.log(total)//[24]

 

Currying and partial application

Terminology

Unary function

A function that only accepts one parameter is called an unary function

Binary function

A function that accepts two parameters is called a binary function

Variadic function

A variadic function is a function that accepts a variable number

const variadic = (a,...variadic){
    console.log(a)
    console.log(variadic)
}
 

Currying

Currying is the process of converting a multi-parameter function into a nested unary function

//
const add = (x,y)=>x+y
add(1,4)

// 

const addCurried = x => y => x + y 
addCurried(1)(3)

//. add  addCurried  

const curry = (binarFn)=>{
  return function(firstArg){
    return function(secondArg){
      binarFn(firstArg,firstArg)
    }
  }
}

let autoCurriedAdd = curry(add)
autoCurriedAdd(2)(3)
    
 

Implement multi-parameter function currying

const curry = (fn)=>{
  if(typeof fn !== 'function'){
    throw Error('No function pvovided')
  }


  return function curriedFn(...args){
   // 
    if(args.length < fn.length){
   // 
      return function (...argsOther){
        return curriedFn.apply(null,args.concat(argsOther))
      }
    }else{
      return fn.apply(null,args)
    }
  }
}


const multiply = (x,y,z) => x * y * z;
console.log(curry(multiply)(2)(3)(4))

 

Currying application example Find numbers in an array

let match = curry(function (expr,str) {
  return str.match(expr)
})
let hasNumber = match(/[0-9]+/)

let initFilter = curry(function (fn,array) {
  return array.filter(fn)
})

let findNumberInArray = initFilter(hasNumber)
console.log(findNumberInArray(['aaa', 'bb2', '33c', 'ddd', ]))

 

data flow

Partial application

The currying function designed above always accepts an array at the end, which makes the list of parameters it can accept can only be from the left to the right

But sometimes, we can't pass in parameters strictly from left to right, or we just want to partially apply function parameters. Here we need to use the concept of partial application, which allows developers to partially apply function parameters.

    
const partial = function (fn,...partialArgs){
  return function (...fullArguments){
    let args = partialArgs;
    let arg = 0;
    for(let i= 0;i < args.length && arg < fullArguments.length; i++){
      if(args[i] === undefined){
        args[i] = fullArguments[arg++]
      }
    }

    return fn.apply(null,args)
  }
}

// JSON
let prettyPrintJson = partial(JSON.stringify,undefined,null,2)
console.log(prettyPrintJson({name:'fangxu',gender:'male'}))

// 
{
  "name": "fangxu",
  "gender": "male"
}


 

Combination and pipeline

Unix philosophy

The idea of Unix is a set of ideas proposed by Ken Thompson.

  • Each program only does one thing. In order to accomplish this task, it is better to rebuild the complex old program by adding new "attributes"
  • The output of each program should be the input of another program that is not yet known

Compose function

The functions combined by compose are called from right to left in the order passed in. So the incoming fns needs to be reversed first, and then we use reduce. The initial value of reduce's accumulator is value, and then (acc,fn) => fn(acc) will be called, and fn will be taken out from the fns array in turn, and will be accumulated The current value of the device is passed to fn, that is, the return value of the previous function is passed to the parameter of the next function.

Basic Edition

const compose = (a,b)=>
    (c)=>a(b(c))

//
let number =compose(Math.round,parseFloat)
console.log(number('3.34'))//3
 

Multi-function version


const compose = (...fns)=>
  (value)=>
    reduce(fns.reverse(),(acc,fn)=>fn(acc),value )
 

Pipeline/sequence

The data flow of the compose function is from right to left, and the rightmost function is executed first. We can also make the leftmost function execute first, and the rightmost function later. This process of processing data flow from left to right It is called a pipeline or sequence.

// compose fns.reverse()
const pipe = (...fns) => (value) => reduce(fns,(acc,fn) => fn(acc),value)
 

Functors

It will help us deal with errors in a purely functional way

What is a functor

Definition: A functor is an ordinary object (in other languages, it may be a class), which implements the map function and generates a new object when it is worth traversing each object.

Realization functor

1. In short, a functor is a container that holds a value. And the functor is an ordinary object. We can create a container (that is, an object) so that it can hold any value passed to it.


const Container = function(val){
    this.value = val
}

let testValue = new Container(2)
//=> Container {value:2
 

Add a static method to Container, which can omit the new keyword when creating a new Container

Container.of = function (value) {
  return new Container(value)
}

// 
Container.of(1)
//=> Container {value:1}
 

2.The function needs to implement the map method.The specific implementation is that the map function takes the value from the Container, and the passed function calls the taken value as a parameter, and puts the result back into the Container.

Container.prototype.map = function (fn){
  return Container.of(fn(this.value))
}

//  double  
let double = (x) => x + x;
Container.of(3).map(double)
//=> Container {value: 6}
 

3.map returns the execution result of the passed function as a worthy Container instance, so it can be chained

Container.of(3).map(double).map(double).map(double)
//=> Container {value: 24}
 

**Through the above implementation, we can find that the functor is an object that implements the map contract. Functor is a concept that seeks a contract. The contract is very simple, that is, to implement map **

MayBe functors

It will deal with errors in the code more functionally

Will judge the value of undefined and null

//MayBe   Container  
const MayBe = function (value) {
  this.value = value
}
MayBe.of = function (value) {
  return new MayBe(value)
}
// isNothing
MayBe.prototype.isNoting = function () {
  return this.value === null || this.value === undefined;
}
//  map,  map  
MayBe.prototype.map = function(fn) {
  return this.isNoting()?MayBe.of(null):MayBe.of(fn(this.value))
}

//MayBe 
let value = 'string';
MayBe.of(value).map(upperCase)
//=> MayBe { value: 'STRING' }
let nullValue = null
MayBe.of(nullValue).map(upperCase)
//  MayBe { value: null }


 

Either functors

MayBe.of("tony")
  .map(() => undefined)
  .map((x)f => "Mr. " + x)
 

The result of the above code is MyaBe {value:null}that this is just a simple example. If the code is more complicated, we cannot know which branch failed to check the undefined or null value. At this time, the Either functor is needed, which can solve the problem of branch expansion.

const Nothing = function (value) {
  this.value = value;
}
Nothing.of = function (value) {
  return new Nothing(value)
}
Nothing.prototype.map = function (fn) {
  return this;
}
const Some = function (value) {
  this.value = value;
}
Some.of = function (value) {
  return new Some(value)
}
Some.prototype.map = function (fn) {
  return Some.of(fn(this.value));
}

const Either = {
  Some,
  Nothing
}

 

Pointed functors

A functor is just an interface that implements the map contract. Pointed functor is also a subset of functor, which has an interface that implements the of contract. We also implemented the of method in MayBe and Either to avoid using the new keyword when creating a Container. So both MayBe and Either can be called Pointed functors.

ES6   Array.of    Pointed  
 

Monad functors

MayBe functors are likely to be nested. If nesting occurs, it is difficult for us to continue to manipulate the real value. Must go deep into MayBe to operate.

let joinExample = MayBe.of(MayBe.of(5));
//=> MayBe { value: MayBe { value: 5 } }

// 5 4  MayBe  
joinExample.map((insideMayBe) => {
  return insideMayBe.map((value) => value + 4)
})
//=> MayBe { value: MayBe { value: 9 } }

 

At this time, we can implement a join method to solve this problem.

//  isNothing   value
MayBe.prototype.join = function () {
  return this.isNoting()? MayBe.of(null) : this.value
}


let joinExample2 = MayBe.of(MayBe.of(5));
//=> MayBe { value: MayBe { value: 5 } }

// 5 4 
joinExample2.join().map((value) => value + 4)
//=> MayBe { value: 9 }

 

To extend it a bit, we extend a chain method.


MayBe.prototype.chain = function (fn) {
  return this.map(fn).join()
}
 

After calling chain, the nested MayBe can be expanded

let joinExample3 = MayBe.of(MayBe.of(5));
//=> MayBe { value: MayBe { value: 5 } }


joinExample3.chain((insideMayBe) => {
  return insideMayBe.map((value) => value + 4)
})
//=> MayBe { value: 9 }


 

Monad is actually a functor with chain method. Only MayBe of of and map is a functor

reference

Concluding remarks

A pupil in the front end!!!