Why learn functional programming

Functional programming is a very old concept, long before the birth of the first computer, the history of functional programming.

So why are we learning functional programming now?

  • 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 for easy testing and parallel processing
  • There are many libraries to help us with functional development: Lodash, underscore, Ramda

What is functional programming

Functional Programming (FP), FP is one of the Programming paradigms, we often hear of procedural Programming, object-oriented Programming.

  • The way of thinking of object-oriented programming: abstract the things in the real world into classes and objects in the program world, and demonstrate the connection of things and events through encapsulation, inheritance and polymorphism

  • The way of thinking about functional programming: abstracting things and their relationships from the real world to the program world (visualizing processes)

    • The essence of the program: according to the input through some operation to obtain the corresponding output, the program development process will involve many functions with input and output
    • X -> f -> y, y=f(x)
    • A function in functional programming is not a function **(method) in a program, but a function (mapping) in mathematics, such as y = sin(x), the relation between x and y
    • The same input always yields the same output **(pure function)**
    • Functional programming is used to describe mappings between data (functions)
// Non-functional
let num1 = 2
let num2 = 3
let sum = num1 + num2 
console.log(sum)

/ / function type
function add (n1, n2) {
  return n1 + n2
}
let sum = add(2.3) 
console.log(sum)
Copy the code

Front knowledge

  • The function is a first-class citizen
  • Higher-order functions
  • closure

Functional first-class citizen

In JavaScript, a Function is just an ordinary object (via new Function()), we can store the Function in a variable/array, and it can be used as an argument and return value to another Function, We can even construct a new Function while the program is running via new Function(‘alert(123)’).

A programming language is said to have first-class functions when its functions can be treated as variables. For example, in this language, a function can be passed as an argument to another function, can be a return value of another function, and can be assigned to a variable. From MDN

Expand on the three points

  • Functions can be passed as arguments to other functions
function sayHello() {
   return "Hello, ";
}
function greeting(helloMessage, name) {
  console.log(helloMessage() + name);
}
// Pass 'sayHello' as an argument to the 'greeting' function
greeting(sayHello, "JavaScript!"); // Hello, JavaScript!
Copy the code
  • Can be returned by another function
function sayHello() {
   return function() {
      console.log("Hello!"); }}Copy the code
  • Can also be assigned to a variable
const foo = function() {
   console.log("foobar");
}
// Call it with a variable
foo();

// An example
const BlogController = {
  index(posts) {
    return Views.index(posts);
  },
  show(post) {
    return Views.show(post);
  },
  create(attrs) {
    return Db.create(attrs);
  },
  update(post, attrs) {
    return Db.update(post, attrs);
  },
  destroy(post) {
    returnDb.destroy(post); }};/ / optimization
const BlogController = {
  index: Views.index,
  show: Views.show,
  create: Db.create,
  update: Db.update,
  destroy: Db.destroy,
};

Copy the code

Tips: Functions are first-class citizens are the basis for higher order functions, Currization, etc.

Higher-order functions

What is a higher-order function

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

Example:

  • Function as argument
// forEach
function forEach(array, fn) {
  for (let i = 0; i < array.length; i++) { fn(array[i]); }}// filter
function filter(array, fn) {
  let results = [];
  for (let i = 0; i < array.length; i++) {
    if(fn(array[i])) { results.push(array[i]); }}return results;
}

Copy the code
  • Function as the return value
function makeFn() {
  let msg = "Hello function";
  return function () {
    console.log(msg);
  };
}
const fn = makeFn();
fn();

Copy the code
// once
function once(fn) {
  let done = false;
  return function () {
    if(! done) { done =true;
      return fn.apply(this.arguments); }}; }let pay = once(function (money) {
  console.log(` pay:${money} RMB`);
});

// Will only be paid once
pay(5);
pay(5);
pay(5);

Copy the code

If you are interviewing, you may ask: How do I make a function execute only once? How to implement Once monitoring for EventEmitter

The meaning of using higher-order functions

  • Abstraction can help us mask the details and focus only on our goals
  • Higher-order functions are used to abstract general problems
// 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
let array = [1.2.3.4];
forEach(array, (item) = > {
  console.log(item);
});
let r = filter(array, (item) = > {
  return item % 2= = =0;
});

Copy the code

Higher-order functions are commonly used

  • forEach
  • map
  • filter
  • every
  • some
  • find/findIndex
  • reduce
  • sort ……
const map = (array, fn) = > {
  let results = [];
  for (const value of array) {
    results.push(fn(value));
  }
  return results;
};
const every = (array, fn) = > {
  let result = true;
  for (const value of array) {
    result = fn(value);
    if(! result) {break; }}return result;
};
const some = (array, fn) = > {
  let result = false;
  for (const value of array) {
    result = fn(value);
    if (result) {
      break; }}return result;
};

Copy the code

closure

  • Closure: A Closure is formed by binding a function with references to its surrounding state (lexical context).
    • You can call an inner function of a function from another scope and access members of that function’s scope
// function as return value
function makeFn() {
  let msg = "Hello function";
  return function () {
    console.log(msg);
  };
}
const fn = makeFn();
fn();
Copy the code

You can see that when Viewed by DevTools there is a Closure, indicating that a Closure has been generated.

  • The essence of closures: functions are placed on an execution stack at execution time and removed from the stack when the function completes execution, but scoped members on the heap cannot be freed because of external references, so internal functions can still access the members of the external function
  • Closure case
// The first number is base salary, and the second number is merit pay
function makeSalary (x) {
    return function (y) {
      return x + y
} }
let salaryLevel1 = makeSalary(1500) 
let salaryLevel2 = makeSalary(2500)
console.log(salaryLevel1(2000)) 
console.log(salaryLevel1(3000))
Copy the code

Pure functions

Pure functions: The same input always yields the same output without any observable side effects

  • A pure function is like a function in mathematics, y = f(x).

  • Lodash is a library of pure functions that provides methods for operating on arrays, numbers, objects, strings, functions, and more
  • An array ofslicespliceThey are: pure function and impure function
    • Slice returns the specified portion of the array without altering the original array

    • Splice operates on an array and returns that array, changing the original array

A simple way to determine if a function is pure is to call it multiple times and return the same value

let numbers = [1.2.3.4.5] / / pure functions
numbers.slice(0.3) // => [1, 2, 3] 
numbers.slice(0.3) // => [1, 2, 3]
numbers.slice(0.3) // => [1, 2, 3]
// impure function
numbers.splice(0.3) // => [1, 2, 3] 
numbers.splice(0.3) / / = > [4, 5]
numbers.splice(0.3) / / = > []
Copy the code
  • Functional programming does not preserve the middle of the calculation, so variables are immutable (stateless)
  • We can hand off the results of one function to another

The benefits of pure functions

  • cacheable
    • Because pure functions always have the same result on the same input, the result of a pure function can be cached
const _ = require("lodash");
function getArea(r) {
  return Math.PI * r * r;
}
let getAreaWithMemory = _.memoize(getArea);
console.log(getAreaWithMemory(4));

Copy the code
  • Create your own memoize function
function memoize(f) {
  let cache = {};
  return function () {
    let arg_str = JSON.stringify(arguments);
    cache[arg_str] = cache[arg_str] || f.apply(f, arguments);
    return cache[arg_str];
  };
}
Copy the code
  • testable
    • Pure functions make testing easier
  • Parallel processing
    • Parallel manipulation of shared memory data in a multithreaded environment is likely to cause unexpected problems
    • Pure functions do not need access to shared memory data, so they can run at will in parallel (Web workers)

Side effects

  • Pure functions: For the same input you always get the same output without any observable side effects
/ / not pure
let mini = 18;
function checkAge(age) {
  return age >= mini;
}
// Pure (hardcoded, later can be solved by Currization)
function checkAge(age) {
  let mini = 18;
  return age >= mini;
}
Copy the code

Side effects make a function impure (as in the example above). Pure functions return the same output based on the same input, and can have side effects if the function depends on external state and cannot guarantee the same output. Sources of side effects:

  • The configuration file
  • The database
  • Get user input
  • .

All external interactions may bring side effects, which also reduce the generality of the method and make it unsuitable for expansion and reusability. At the same time, side effects will bring security risks to the program and uncertainty to the program. However, side effects cannot be completely prohibited, and they should be controlled as much as possible.

In summary, the side effects come from the word “external.”

Haskell Brooks Curry

Use Coriation to solve the hard-coded problem in the previous case

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;
  };
}
/ / ES6 writing
let checkAge = (min) = > (age) = > age >= min;
let checkAge18 = checkAge(18);
let checkAge20 = checkAge(20);
checkAge18(24);
checkAge18(20);

Copy the code

Currie (Currying) :

  • 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

The Currization function in Lodash

  • _.curry(func)
    • Function: creates a function that takes one or more arguments from func, executes func if all arguments needed by func are supplied and returns the result of the execution. Otherwise continue to return the function and wait for the remaining arguments to be received.
    • Parameters: functions that require currization
    • Return value: The currified function
const _ = require("lodash"); // The currified function
function getSum(a, b, c) {
  return a + b + c;
}
// The currie function
let curried = _.curry(getSum); / / test
curried(1.2.3);
curried(1) (2) (3);
curried(1.2) (3);

Copy the code
  • case
const _ = require("lodash");
const match = _.curry(function (reg, str) {
  return str.match(reg);
});
const haveSpace = match(/\s+/g);
const haveNumber = match(/\d+/g);
console.log(haveSpace("hello world"));
console.log(haveNumber("25$"));
const filter = _.curry(function (func, array) {
  return array.filter(func);
});
console.log(filter(haveSpace, ["John Connor"."John_Donne"]));
const findSpace = filter(haveSpace);
console.log(findSpace(["John Connor"."John_Donne"]));

Copy the code
  • Simulate the implementation of _.curry()
function curry(func) {
  return function curriedFn(. args) {
    // Determine the number of arguments and parameters
    if (args.length < func.length) {
      return function () {
        returncurriedFn(... args.concat(Array.from(arguments)));
      };
    }
    // Call func with the same number of arguments
    returnfunc(... args); }; }Copy the code
  • conclusion
    • Corrification allows us to pass in fewer arguments to a function and get a new function that has memorized some of the fixed arguments
    • This is a kind of ‘cache’ of function parameters
    • Make the function more flexible, make the function smaller granularity
    • You can convert multivariate functions into unary functions, and you can combine functions to produce powerful functions

Function composition

  • Pure functions and Currization are easy to write onion code h(g(f(x)).
    • Take the last element of the array and convert it to uppercase,.toUpper(.first(_.reverse(array)))

  • Function composition allows us to recombine fine-grained functions to generate a new function

The pipe

The following figure shows the process of using functions to process data in the program. The fn function is given parameter A and the result b is returned. You can think of data A going through a pipe to get data B.

When the fn function is complicated, we can divide the function fn into several smaller functions, and there are more m and N generated in the intermediate operation process. In the figure below, it can be imagined that the pipe FN is split into three pipes F1, F2 and F3. Data A obtains the result M through pipe F3, m obtains the result N through pipe F2, and N obtains the final result B through pipe F1

fn=compose(f1,f2,f3) 
b=fn(a)
Copy the code

Function composition

  • Compose: If a function needs to be processed by multiple functions to get the final value, the intermediate functions can be combined into oneA 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
// combine functions
function compose (f, g) {
  return function (x) {
    return f(g(x))
} }
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
  • Combinatorial functions in Lodash
  • Lodash combines the flow() or flowRight() functions, both of which can combine multiple functions
  • Flow () runs from left to right
  • FlowRight () runs from right to left and is used more often
const _ = require('lodash')
const toUpper = s= > s.toUpperCase() 
const reverse = arr= > arr.reverse() 
const first = arr= > arr[0]
const f = _.flowRight(toUpper, first, reverse) 
console.log(f(['one'.'two'.'three']))
Copy the code
  • Simulate the flowRight method for implementing LoDash
// Multiple function combinations
function compose (. fns) {
    return function (value) {
        return fns.reverse().reduce(function (acc, fn) {
      return fn(acc)
    }, value)
} }
// ES6
const compose = (. fns) = > value= > fns.reverse().reduce((acc, fn) = > fn(acc), value)
Copy the code
  • The combination of functions must satisfy associativity:
    • We can either combine g with H, or f with g, and it’s the same thing

// Associativity
let f = compose(f, g, h)
let associative = compose(compose(f, g), h) == compose(f, compose(g, h)) // true
Copy the code
  • So the code could also look like this
const _ = require('lodash')
// const f = _.flowRight(_.toUpper, _.first, _.reverse)
// const f = _.flowRight(_.flowRight(_.toUpper, _.first), _.reverse)
const f = _.flowRight(_.toUpper, _.flowRight(_.first, _.reverse))
console.log(f(['one'.'two'.'three'])) // => THREE
Copy the code
  • debugging
const _ = require('lodash')
const trace = _.curry((tag, v) = > { 
    console.log(tag, v)
    return v
})
const split = _.curry((sep, str) = > _.split(str, sep)) 
const join = _.curry((sep, array) = > _.join(array, sep)) 
const map = _.curry((fn, array) = > _.map(array, fn))
const f = _.flowRight(join(The '-'), trace('after the map'), map(_.toLower), trace('after the split), split(' '))
console.log(f('NEVER SAY DIE'))
Copy the code
  • lodash/fp
    • Lodash’s FP module provides a practical approach to functional programming friendly
    • Immutable auto-curried iteratee-first data-last method is provided
/ / lodash module
const _ = require('lodash')
_.map(['a'.'b'.'c'], _.toUpper) // => ['A', 'B', 'C'] _.map(['a', 'b', 'c'])
// => ['a', 'b', 'c']
_.split('Hello World'.' ') 
/ / lodash/fp module
const fp = require('lodash/fp') 
fp.map(fp.toUpper, ['a'.'b'.'c'])
fp.map(fp.toUpper)(['a'.'b'.'c'])
fp.split(' '.'Hello World') 
fp.split(' ') ('Hello World')
Copy the code
const fp = require('lodash/fp')
const f = fp.flowRight(fp.join(The '-'), fp.map(_.toLower), fp.split(' ')); console.log(f('NEVER SAY DIE'))
Copy the code

Point Free

Point Free: We can define the process of data processing as a composite operation unrelated to data, without using the parameter representing data, as long as the simple operation steps are combined together. Before using this mode, we need to define some auxiliary basic operation functions.

  • There is no need to specify the data to be processed
  • You just need to synthesize the operation
  • You need to define some auxiliary basic operation functions
constf=fp.flowRight(fp.join(The '-'),fp.map(_.toLower),fp.split(' '))
Copy the code

Case presentation

// Non-point Free mode
// Hello World => hello_world 
function f (word) {
    return word.toLowerCase().replace(/\s+/g.'_');
}
// Point Free
const fp = require('lodash/fp')
const f = fp.flowRight(fp.replace(/\s+/g.'_'), fp.toLower) 
console.log(f('Hello World'))
Copy the code
  • Using the Point Free pattern, extract and capitalize the first letter of the word
const fp = require('lodash/fp')
const firstLetterToUpper = fp.flowRight(join('. '),
fp.map(fp.flowRight(fp.first, fp.toUpper)), split(' '))
console.log(firstLetterToUpper('world wild web')) // => W. W. W
Copy the code

functor

Why do we learn functors

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.

What is a Functor

  • Container: contains values and deformation relations of values (the deformation relations are functions)
  • Functor: is a special container implemented by an ordinary object that hasmapMethod,mapMethod can run a function to manipulate the value (deformation relationship)
// A container that wraps a value
class Container {
  // of static methods that create objects without the new keyword
  static of(value) {
    return new Container(value);
  }
  constructor(value) {
    this._value = value;
  }
  // the map method, passing in the deformation relation, maps each value in the container to another container
  map(fn) {
    return Container.of(fn(this._value)); }}/ / test
Container.of(3)
  .map((x) = > x + 2)
  .map((x) = > x * x);

Copy the code
  • Conclusion:
    • The operations of functional programming do not operate directly on values, but are performed by functors
    • A functor is an object that implements the Map contract
    • We can think of a functor as a box that encapsulates a value
    • To process the values in the box, we need to pass a value-handling function (pure function) to the box’s Map method, which will process the values
    • The final map method returns a box containing the new value (functor)
  • In Functor if we pass null or undefined
// value if accidentally passed a null value (side effect)
Container.of(null).map(x= > x.toUpperCase())
// TypeError: Cannot read property 'toUpperCase' of null
Copy the code

MayBe functor

  • We may 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.
class MayBe {
  static of(value) {
    return new MayBe(value);
  }
  constructor(value) {
    this._value = value;
  }
  // Return a null functor if the null value is distorted
  map(fn) {
    return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value));
  }
  isNothing() {
    return this._value === null || this._value === undefined; }}// Pass in a specific value
MayBe.of("Hello World")
  .map((x) = > x.toUpperCase()) // MayBe. Of (null)
  .map((x) = > x.toUpperCase()); // => MayBe { _value: null }

Copy the code
  • In the MayBe functor, it is difficult to confirm which step produces the null value problem, as shown in the following example:
MayBe.of('hello world') 
    .map(x= > x.toUpperCase()) 
    .map(x= > null)
    .map(x= > x.split(' '))
// => MayBe { _value: null }
Copy the code

Either functor

  • Either of the two, similar to if… else… The processing of
  • Exceptions make functions impure, and Either functors can be used for exception handling
class Left {
  static of(value) {
    return new Left(value);
  }
  constructor(value) {
    this._value = value;
  }
  map(fn) {
    return this; }}Copy the code
class Right {
  static of(value) {
    return new Right(value);
  }
  constructor(value) {
    this._value = value;
  }
  map(fn) {
    return Right.of(fn(this._value)); }}Copy the code
  • Either is used to handle exceptions
function parseJSON(json) {
  try {
    return Right.of(JSON.parse(json));
  } catch (e) {
    return Left.of({ error: e.message }); }}let r = parseJSON('{ "name": "zs" }').map((x) = > x.name.toUpperCase());
console.log(r);
Copy the code

IO functor

  • The _value in the IO functor is a function, and we’re treating the function as a value
  • IO functors can store impure actions in _value, delay execution of the impure operation (lazy execution), and wrap the current operation
  • Leave impure operations to the caller
const fp = require("lodash/fp");
class IO {
  static of(x) {
    return new IO(function () {
      return x;
    });
  }
  constructor(fn) {
    this._value = fn;
  }
  map(fn) {
    // Combine the current value and the incoming fn into a new function
    return new IO(fp.flowRight(fn, this._value)); }}/ / call
let io = IO.of(process).map((p) = > p.execPath);
console.log(io._value());

Copy the code

Task asynchronous execution

  • The implementation of asynchronous tasks is too complex and we use Tasks in Folktale to demonstrate this
  • Folktale is a standard functional programming library
    • Unlike Lodash and Ramda, it doesn’t offer many features
    • It provides only a few functionally handled operations, such as compose, Curry, etc., some functor tasks, Either, MayBe, etc
const { compose, curry } = require("folktale/core/lambda");
const { toUpper, first } = require("lodash/fp");
// The first argument is the number of arguments passed to the function
let f = curry(2.function (x, y) {
  console.log(x + y);
});
f(3.4);
f(3) (4);
// Function combination
let f = compose(toUpper, first);
f(["one"."two"]);
Copy the code
  • Task asynchronous execution
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

Pointed functor

The conservative functor is a functor that implements the static of method. The of method is used to avoid using new to create objects. The deeper point is that the of method is used to put values into Context(put values into containers and use maps to process values).

class Container {
  static of (value) {
    return new Container(value) 
  }
......
}
Contanier.of(2) .map(x= > x + 5)
Copy the code

Monad (list)

When using IO functors, if we write the following code:

const fs = require("fs");
const fp = require("lodash/fp");
let readFile = function (filename) {
  return new IO(function () {
    return fs.readFileSync(filename, "utf-8");
  });
};
let print = function (x) {
  return new IO(function () {
    console.log(x);
    return x;
  });
};

// IO(IO(x))
let cat = fp.flowRight(print, readFile);
/ / call
let r = cat("package.json")._value()._value();
console.log(r);

Copy the code
  • The Monad functor is a Pointed functor that can be flattened, IO(IO(x))
  • A functor is a Monad if it has both join and of methods and obeies some laws
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