Original text: medium.com/better-prog…

Translator: Front-end wisdom

Click “like” and then look, wechat search [Big Move the world] pay attention to this person without dACHang background, but with a positive attitude upward. In this paper, making github.com/qq449245884… Has been included, the article has been categorized, also organized a lot of my documentation, and tutorial materials.

Everyone said there was no project on your resume, so I found one and gave it away【 Construction tutorial 】.

After a long period of learning and using object-oriented programming, let’s take a step back and consider system complexity.

After doing some research, I discovered the concepts of functional programming, such as immutability and pure functions. These concepts enable you to build functions without side effects, so it is easier to maintain systems with other benefits.

In this article, you’ll cover functional programming and some of the key concepts in detail through extensive code examples.

What is functional programming

Functional programming is a programming paradigm, a style of constructing the structure and elements of a computer program that treats computation as an evaluation of mathematical functions, avoiding changes in state and mutable data.

Pure functions

The first basic concept we need to know when we want to understand functional programming is pure functions, but what are pure functions?

How do we know if a function is pure? Here’s a very strict definition:

  • If given the same parameters, the same result is returned (also known as deterministic).

  • It doesn’t cause any side effects.

Given the same parameters, you get the same result

If given the same arguments, it returns the same result. Imagine that we want to implement a function that calculates the area of a circle.

Not a pure function would do this, take radius as an argument, and then compute radius * radius * PI:

Let PI = 3.14; const calculateArea = (radius) => radius * radius * PI; calculateArea(10); / / returns 314.0Copy the code

Why is this an impure function? The reason is simple, because it uses a global object that is not passed to the function as an argument.

Now, imagine that some mathematicians thought that the value of PI was actually 42 and changed the value of the global object.

So the impure function is 10 times 10 times 42 is 4200. For the same parameter (radius = 10), we get different results.

Fix it:

Let PI = 3.14; const calculateArea = (radius, pi) => radius * radius * pi; calculateArea(10, PI); / / returns 314.0Copy the code

Now pass the value of PI to the function as an argument, so that no external objects are introduced.

  • For the parametersradius = 10andPI = 3.14, always get the same result:314.0.
  • forradius = 10PI = 42, always get the same result:4200

Read the file

The following function reads the external file. It is not a pure function, and the contents of the file may vary from time to time.

const charactersCounter = (text) => `Character count: ${text.length}`;

function analyzeFile(filename) {
  let fileContent = open(filename);
  return charactersCounter(fileContent);
}
Copy the code

Random number generation

Any function that relies on a random number generator cannot be pure.

Function Yearengenie () {if (math.random () > 0.5) {return "You get a raise!" ; } else { return "Better luck next year!" ; }}Copy the code

No obvious side effects

Pure functions do not cause any observable side effects. Examples of visible side effects include modifying global objects or parameters passed by reference.

Now, let’s implement a function that takes an integer and increments that integer and returns it.

let counter = 1; function increaseCounter(value) { counter = value + 1; } increaseCounter(counter); console.log(counter); / / 2Copy the code

The impure function takes the value and reassigns counter, increasing its value by 1.

Functional programming discourages variability. We modify the global object, but how do we make it pure? Just return the increment of 1.

let counter = 1; const increaseCounter = (value) => value + 1; increaseCounter(counter); // 2 console.log(counter); / / 1Copy the code

The pure function increaseCounter returns 2, but the counter value is still the same. The function returns an increasing value without changing the value of the variable.

If we follow these two simple rules, it will be much easier to understand our program. Now each function is isolated and cannot affect the rest of the system.

Pure functions are stable, consistent, and predictable. Given the same arguments, pure functions always return the same result.

We don’t need to worry about different results for the same parameters, because it never happens.

The benefits of pure functions

Pure function code is definitely easier to test and doesn’t need to mock anything, so we can unit test pure functions in different contexts:

  • Given a parameterA, the expected return value of the functionB
  • Given a parameterC, the expected return value of the functionD

A simple example would be to take a set of numbers and add one to each number as a sand sculpture.

let list = [1, 2, 3, 4, 5];

const incrementNumbers = (list) => list.map(number => number + 1);
Copy the code

Takes an array of numbers, increments each number with map, and returns a new list of incrementing numbers.

incrementNumbers(list); // [2, 3, 4, 5, 6]
Copy the code

For the input [1,2,3,4,5], the expected output is [2,3,4,5,6].

immutability

Even though time changes or doesn’t change, pure functions don’t change.

When data is immutable, its state cannot be changed after creation.

We can not change immutable objects, if you have to hard, just need to make a deep copy of the copy, and then operate the copy.

In JS, we usually use a for loop, where each iteration of for I is a mutable variable.

var values = [1, 2, 3, 4, 5];
var sumOfValues = 0;

for (var i = 0; i < values.length; i++) {
  sumOfValues += values[i];
}

sumOfValues // 15
Copy the code

With each iteration, I and sumOfValue states are changing, but how do we handle variability in the iteration? The answer is to use recursion.

let list = [1, 2, 3, 4, 5]; let accumulator = 0; function sum(list, accumulator) { if (list.length == 0) { return accumulator; } return sum(list.slice(1), accumulator + list[0]); } sum(list, accumulator); // 15 list; // [1, 2, 3, 4, 5] accumulator; / / 0Copy the code

The above code has a sum function that takes a vector of numbers. The function calls itself until the list is empty and exits recursion. For each iteration, we will add the value to the total Accumulator.

Using recursion, we keep the variables constant. The List and Accumulator variables are not changed. It keeps the same value.

Observation: We can do this using Reduce. This will be discussed in the next section on higher-order functions.

The final state of the build object is also common. Suppose we have a string that we want to convert to a URL slug.

In Ruby object-oriented programming, we can create a class UrlSlugify that has a slugify method to convert string input into URL slug.

class UrlSlugify attr_reader :text def initialize(text) @text = text end def slugify! text.downcase! text.strip! text.gsub! (' ', '-') end end UrlSlugify.new(' I will be a url slug ').slugify! # "i-will-be-a-url-slug"Copy the code

The imperative programming approach used above begins with lowercase letters indicating what we want to do in each slugify process, then removes useless Spaces, and finally replaces any remaining Spaces with a hyphen.

This way changes the input state in the whole process, and clearly does not conform to the concept of pure functions.

This side can be optimized by combinations of functions or chains of functions. In other words, the result of a function is used as input to the next function, without modifying the original input string.

const string = " I will be a url slug   ";

const slugify = string =>
  string
    .toLowerCase()
    .trim()
    .split(" ")
    .join("-");

slugify(string); // i-will-be-a-url-slug
Copy the code

The code above does a few things:

  • ToLowerCase: Converts string to all lowercase letters.

  • Trim: Deletes whitespace at both ends of the string.

  • Split and Join: Replaces all matching instances with substitutions in the given string

Referential transparency

Then we implement a square function:

const square = (n) => n * n;
Copy the code

Given the same input, the pure function always has the same output.

square(2); // 4 square(2); // 4 square(2); / / 4 / /...Copy the code

Passing 2 as an argument to the square function always returns 4. So we can replace square(2) with 4, and our function is referential transparent.

Basically, a function can be considered transparent if it consistently produces the same result for the same input.

So with that in mind, one cool thing we can do is memorize this function. Let’s say I have a function that looks like this

const sum = (a, b) => a + b;
Copy the code

Call it with these arguments

sum(3, sum(5, 8));
Copy the code

Sum (5, 8) is always 13, so we can do something else:

sum(3, 13);
Copy the code

This expression always gets 16, so we can replace the whole expression with a numerical constant and write it down.

Functions are first-class citizens in JS

As first-class citizens of JS, functions can also be treated as values and used as data.

  • Reference it from constants and variables.
  • Pass it as an argument to other functions.
  • Return it as a result of some other function.

The idea is to treat functions as values and pass them as data. In this way, we can combine different functions to create new functions with new behavior.

Suppose we have a function that sums two values and then doubles them as follows:

const doubleSum = (a, b) => (a + b) * 2;
Copy the code

Take the difference between the two values and then double the values:

const doubleSubtraction = (a, b) => (a - b) * 2;
Copy the code

These functions have similar logic, but differ in the function of the operators. If we can treat functions as values and pass them as arguments, we can build a function that takes the operator function and uses it inside the function.

const sum = (a, b) => a + b; const subtraction = (a, b) => a - b; const doubleOperator = (f, a, b) => f(a, b) * 2; doubleOperator(sum, 3, 1); // 8 doubleOperator(subtraction, 3, 1); / / 4Copy the code

The f argument is applied to a and B. Here the sum and subtraction function are passed and the doubleOperator is used to combine and create the new behavior.

Higher-order functions

When we talk about higher-order functions, we usually include the following:

  • Take one or more functions as arguments

  • Return a function as the result

The doubleOperator function implemented above is a higher-order function because it takes an operator function as an argument and uses it.

The filter, map, and reduce functions we often use are higher-order functions, Look, see, see.

Filter

For a given collection, we want to filter by attributes. The filter function expects a true or false value to determine whether an element should be included in the result set.

If the callback expression is true, the filter function will include elements in the result set; otherwise, it will not.

A simple example is, when we have a set of integers, we only want even numbers.

imperative

To get all the even numbers in an array, we usually do this:

  • Create an empty array evenNumbers

  • Go over the numbers

  • Push an even number into an evenNumbers array

    var numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; var evenNumbers = [];

    for (var i = 0; i < numbers.length; i++) { if (numbers[i] % 2 == 0) { evenNumbers.push(numbers[i]); }}

    console.log(evenNumbers); // (6) [0, 2, 4, 6, 8, 10]

We can also use the filter higher-order function to receive even functions and return an even list:

const even = n => n % 2 == 0; const listOfNumbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; listOfNumbers.filter(even); // [0, 2, 4, 6, 8, 10]

One interesting problem I solved on Hacker Rank FP was the Filter Array problem. The problem is to filter a given array of integers and output only those values less than the specified value X.

Imperative approaches usually go something like this:

var filterArray = function(x, coll) {
  var resultArray = [];

  for (var i = 0; i < coll.length; i++) {
    if (coll[i] < x) {
      resultArray.push(coll[i]);
    }
  }

  return resultArray;
}

console.log(filterArray(3, [10, 9, 8, 2, 7, 5, 1, 3, 0])); // (3) [2, 1, 0]
Copy the code

Declarative mode

For the above, we would prefer a more declarative approach to the problem, as follows:

function smaller(number) { return number < this; } function filterArray(x, listOfNumbers) { return listOfNumbers.filter(smaller, x); } let numbers = [10, 9, 8, 2, 7, 5, 1, 3, 0]; filterArray(3, numbers); / / (2, 1, 0]Copy the code

Using this in smaller functions may seem a bit strange at first, but it’s easy to understand.

The second parameter in the filter function represents the value of this, or x, above.

We can also do this using the Map method. Imagine that you have a set of information

let people = [
  { name: "TK", age: 26 },
  { name: "Kaio", age: 10 },
  { name: "Kazumi", age: 30 }
]
Copy the code

We want to filter people whose age is greater than 21, using filter

const olderThan21 = person => person.age > 21;
const overAge = people => people.filter(olderThan21);
overAge(people); // [{ name: 'TK', age: 26 }, { name: 'Kazumi', age: 30 }]
Copy the code

map

The main idea of the map function is to transform sets.

The map method transforms a collection by applying a function to all of its elements and building a new collection based on the returned values.

If we don’t want to filter for people older than 21, we want to display something like this: TK is 26 years old.

To use imperative, we usually do this:

var people = [
  { name: "TK", age: 26 },
  { name: "Kaio", age: 10 },
  { name: "Kazumi", age: 30 }
];

var peopleSentences = [];

for (var i = 0; i < people.length; i++) {
  var sentence = people[i].name + " is " + people[i].age + " years old";
  peopleSentences.push(sentence);
}

console.log(peopleSentences); // ['TK is 26 years old', 'Kaio is 10 years old', 'Kazumi is 30 years old']
Copy the code

A declarative would do this:

const makeSentence = (person) => `${person.name} is ${person.age} years old`;

const peopleSentences = (people) => people.map(makeSentence);
  
peopleSentences(people);
// ['TK is 26 years old', 'Kaio is 10 years old', 'Kazumi is 30 years old']
Copy the code

The whole idea is to convert a given array into a new array.

Another interesting HackerRank problem is the update list problem. We want to update the value of an array with its absolute value.

For example, the input [1,2,3, -4,5] needs the output to be [1,2,3,4,5], and the absolute value of -4 is 4.

A simple solution is to update the values in each collection in place, which is dangerous

var values = [1, 2, 3, -4, 5];

for (var i = 0; i < values.length; i++) {
  values[i] = Math.abs(values[i]);
}

console.log(values); // [1, 2, 3, 4, 5]
Copy the code

We use the math.abs function to convert the value to its absolute value and update it in place.

This is not the best solution.

First of all, on the front end we learned about immutability, that immutability makes functions more consistent and predictable, and the idea is to create a new set with all the absolute values.

Second, why not use map here to “transform” all the data

My first thought was to test the math. abs function to handle only one value.

Math.abs(-1); // 1 Math.abs(1); // 1 Math.abs(-2); // 2 Math.abs(2); / / 2Copy the code

We want to convert each value to a positive value (absolute value).

Now that you know how to perform an absolute value operation on a value, you can pass this function as an argument to the Map function.

Remember higher-order functions can take a function as an argument and use it? Yes, the map function can do this

let values = [1, 2, 3, -4, 5];

const updateListMap = (values) => values.map(Math.abs);

updateListMap(values); // [1, 2, 3, 4, 5]
Copy the code

Reduce

The idea of the Reduce function is to take a function and a set and return the value created by combining these items.

A common example is to get the total amount of an order.

Suppose you are at a shopping website and have added products 1, 2, 3, and 4 to your shopping cart (order). Now we need to count the total number of shopping carts:

In an imperative manner, facilitate the order list and add the amount of each item to the total amount.

var orders = [ { productTitle: "Product 1", amount: 10 }, { productTitle: "Product 2", amount: 30 }, { productTitle: "Product 3", amount: 20 }, { productTitle: "Product 4", amount: 60 } ]; var totalAmount = 0; for (var i = 0; i < orders.length; i++) { totalAmount += orders[i].amount; } console.log(totalAmount); / / 120Copy the code

With reduce, we can build a function that handles the volume calculation sum and passes it as a parameter to the Reduce function.

let shoppingCart = [ { productTitle: "Product 1", amount: 10 }, { productTitle: "Product 2", amount: 30 }, { productTitle: "Product 3", amount: 20 }, { productTitle: "Product 4", amount: 60 } ]; const sumAmount = (currentTotalAmount, order) => currentTotalAmount + order.amount; const getTotalAmount = (shoppingCart) => shoppingCart.reduce(sumAmount, 0); getTotalAmount(shoppingCart); / / 120Copy the code

There is the shoppingCart, the function sumAmount that receives the current currentTotalAmount, and the Order object summing over them.

We can also use map to convert the shoppingCart to an amount collection and then use reduce and sumAmount functions.

const getAmount = (order) => order.amount; const sumAmount = (acc, amount) => acc + amount;

function getTotalAmount(shoppingCart) { return shoppingCart .map(getAmount) .reduce(sumAmount, 0); } getTotalAmount(shoppingCart); / / 120Copy the code

GetAmount takes the product object and returns only the amount value, [10,30,20,60], and then reduce combines all the items by adding them up.

Examples of three functions

I looked at how each higher-order function works. Here is an example of how to combine these three functions in a simple example.

Speaking of shopping carts, let’s say we have this list of products in our order

let shoppingCart = [
  { productTitle: "Functional Programming", type: "books", amount: 10 },
  { productTitle: "Kindle", type: "eletronics", amount: 30 },
  { productTitle: "Shoes", type: "fashion", amount: 20 },
  { productTitle: "Clean Code", type: "books", amount: 60 }
]
Copy the code

If you wanted the total of type books in your shopping cart, you would normally do this:

  • Filter for books

  • Use map to convert the shopping cart into an Amount collection.

  • Add up all the items using reduce.

let shoppingCart = [ { productTitle: "Functional Programming", type: "books", amount: 10 }, { productTitle: "Kindle", type: "eletronics", amount: 30 }, { productTitle: "Shoes", type: "fashion", amount: 20 }, { productTitle: "Clean Code", type: "books", amount: 60 } ] const byBooks = (order) => order.type == "books"; const getAmount = (order) => order.amount; const sumAmount = (acc, amount) => acc + amount; function getTotalAmount(shoppingCart) { return shoppingCart .filter(byBooks) .map(getAmount) .reduce(sumAmount, 0); } getTotalAmount(shoppingCart); / / 70Copy the code

The bugs that may exist after code deployment cannot be known in real time. In order to solve these bugs, I spent a lot of time on log debugging. Incidentally, I recommend a good BUG monitoring tool for youFundebug

communication

This article is updated every week, you can search wechat “big move the world” for the first time to read and urge more (one or two earlier than the blog hey), this article GitHub github.com/qq449245884… It has been included and sorted out a lot of my documents. Welcome Star and perfect. You can refer to the examination points for review in the interview.