• Master the JavaScript Interview: What is Functional Programming?
  • By Eric Elliott
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: zoomdong
  • Proofreader: Roc,Long Xiong

Mastering JavaScript Interview: What is Functional Programming

“Mastering JavaScript Interviews” is a series of posts designed to prepare candidates for common questions they may encounter when interviewing for advanced JavaScript positions. These are some of the questions I often ask in real interview scenarios.

Functional programming has become a hot topic in JavaScript. Just a few years ago, few JavaScript programmers even knew what functional programming was, but functional programming ideas are heavily used in every large application code base I’ve seen in the past three years.

Functional programming (often abbreviated FP) is the process of building software by combining pure functions, avoiding state sharing, variable data, and side effects. Functional programming is declarative, not imperative, and application state flows through pure functions. Unlike object-oriented programming, application state is typically shared and collaborated with methods in objects.

Functional programming is a programming paradigm, which means it is a way of thinking about software construction based on some basic, defining principles (as listed above). Other programming paradigms include object-oriented programming and procedural programming.

Functional code tends to be cleaner, more predictable, and easier to test than imperative or object-oriented code — but if you’re not familiar with it and the common patterns associated with it, functional code can also look much denser, and the associated documentation can be incomprehensible to newcomers.

If you start googling functional programming terms, you’ll soon run into a wall of academic jargon that can be downright scary for beginners. To say it has a learning curve is a big understatement. But if you’ve been programming in JavaScript for a while, you’ve probably already used a lot of functional programming concepts and utilities in real software applications.

Don’t let all the new words scare you away. They’re easier than they sound.

The hardest part is absorbing (or understanding) these words. Before you start to understand what functional programming means, there are a lot of concepts to understand in this deceptively simple definition above:

  • Pure functions
  • Function composition
  • Avoiding state sharing
  • Avoid mutable data
  • Avoid side effects

In other words, if you want to know what functional programming means in practice, you must first understand these core concepts.

A pure function is one that has the following characteristics:

  • Given the same input, you always get the same output
  • No side effects

Pure functions have a number of properties that are important in functional programming, including reference transparency (you can use the value of a single call to a function instead of the rest of the calls to that function without affecting the program). Read “What is a Pure Function? Learn more.

A combinatorial function is the process of combining two or more functions to produce a new function or perform some kind of calculation. For example, f.g composition (. Means composition) is equivalent to f(g(x)) in JavaScript. Understanding combinatorial functions is an important step toward understanding how to use functional programming to build software. Read “What is a Combinatorial function?” Learn more.

State sharing

State sharing is the sharing of any variables, objects, or memory space that exists in a scope, or of properties of objects that are passed between scopes. Shared scopes can include global scopes or closure scopes. Typically, in object-oriented programming, objects are shared between scopes by adding properties to other objects.

For example, a computer game might have a main gameobject in which the characters and game items are stored as properties owned by that object. Functional programming avoids state sharing and relies instead on immutable data structures and pure computation to derive new data from existing data. For more details on how functional software handles application state, see “10 Better Redux Architecture Tips.”

The problem with shared state is that in order to understand the effect of a function, you have to know the entire history of every shared variable that the function uses or affects.

Suppose you have a User object that you want to save. The saveUser() function makes a request to the API on the server. During this process, the user changes their personal avatar using updateAvatar() and triggers another saveUser() request. When saved, the server sends back a canonical User object that should replace the corresponding object in memory in order to synchronize changes caused by the server or other client APIS.

Unfortunately, the second response is received before the first, so when the first (now obsolete) response is returned, the new avatar is deleted in memory and replaced with the old avatar. This is an example of a race condition — a very common mistake associated with state sharing.

Another common problem associated with shared state is that changing the order in which functions are called can lead to a series of failures because functions acting on shared state depend on timing:

// In the shared state, the order of function calls changes the result of the function calls.

const x = {
  val: 2
};

const x1 = (a)= > x.val += 1;

const x2 = (a)= > x.val *= 2;

x1();
x2();

console.log(x.val); / / 6

// This example is exactly the same as above except for the object name
const y = {
  val: 2
};

const y1 = (a)= > y.val += 1;

const y2 = (a)= > y.val *= 2;

// The order of function calls is reversed
y2();
y1();

// Change the value of the result
console.log(y.val); / / 5
Copy the code

When state sharing is avoided, the timing and order of function calls do not change the result of calling the function. For pure functions, given the same input, you always get the same output. This allows functions to be called completely independently of other function calls, which can radically simplify changes and refactoring. Changes to one function, or the timing of a function call, do not affect the rest of the program.

const x = {
  val: 2
};

const x1 = x= > Object.assign({}, x, { val: x.val + 1});

const x2 = x= > Object.assign({}, x, { val: x.val * 2});

console.log(x1(x2(x)).val); / / 5


const y = {
  val: 2
};

// Because there is no dependence on external variables
// We don't need different functions to manipulate different variables


// This is intentionally left blank


// Because the function does not change
// So you can call these functions multiple times in any order
// Without having to change the results of other function calls
x2(y);
x1(y);

console.log(x1(x2(y)).val); / / 5
Copy the code

In the example above, we use object.assign () and pass an empty Object as the first argument to copy the attributes of X, rather than making changes on the original data. In the previous example, it was equivalent to creating a new object from scratch without using object.assign(), but this is a common pattern in JavaScript for creating copies of existing state, rather than using mutations, as we demonstrated in the first example.

If you look closely at the console.log() statement in this example, you should notice something I’ve already mentioned: composite functions. Recall that the combinatorial function looks like this: f(g(x)). In this case, we replace the f() and g() in the combination x1.x2 with x1() and x2().

Of course, if you change the order of combinations, the output will also change. The order of operations is also important. F (g(x)) is not always the same as g(f(x)), but what doesn’t matter anymore is what happens to the variables outside the function, that’s important. For impure functions, it is impossible to fully understand what the function does unless you know the entire history of every variable that the function uses or affects.

By removing function call timing dependencies, you eliminate an entire class of potential bugs.

invariance

An immutable object is an object that cannot be modified after being created. In contrast, mutable objects are objects that can be modified after they are created.

Immutability is a core concept of functional programming because without it, the flow of data in a program is lossy. State history is discarded, and strange bugs may creep into your software. For more on the meaning of immutability, see “The Way of Immutability”.

In JavaScript, it’s important not to confuse const with immutability. Const creates a variable name binding that cannot be reassigned after creation. Const does not create immutable objects. You cannot change the object referenced by the binding, but you can still change the properties of the object, which means that a binding created using const is mutable, not immutable.

Immutable objects cannot be changed at all. By deeply freezing an object, you can make the value truly immutable. JavaScript has a way of freezing an object at a layer:

const a = Object.freeze({
  foo: 'Hello'.bar: 'world'.baz: '! '
});

a.foo = 'Goodbye';
// Error: Cannot assign to read only property 'foo' of object Object

Copy the code

But frozen objects are only superficially immutable. For example, the following objects are mutable:

const a = Object.freeze({
  foo: { greeting: 'Hello' },
  bar: 'world'.baz: '! '
});

a.foo.greeting = 'Goodbye';

console.log(`${ a.foo.greeting }.${ a.bar }${a.baz}`);
Copy the code

As you can see, the top basic properties of a frozen object cannot be changed, but any object properties inside it (including arrays, etc.) can still be changed — so even frozen objects are not immutable unless you traverse the entire object tree and freeze every object property.

In many functional programming languages, there is a special immutable data structure called the TRIe data structure (pronounced “tree”), which is essentially deep-frozen — meaning that properties can’t be changed no matter what level in the object hierarchy they are.

Tries uses a shared structure to share references to memory addresses for immutable objects after they have been copied, which uses less memory and improves performance for some operations.

For example, you can perform a consistency comparison at the root node of an object pair to see if two objects are consistent. If they are consistent, you don’t need to traverse the entire object tree looking for differences.

Several libraries in JavaScript use tries, including Immutable. Js and Mori.

I’ve tried both approaches and tend to use immutable.js in large projects that require a lot of Immutable state. For more information, see “10 Better Redux Architecture Tips.”

Side effects

A side effect is any change in application state that is observable outside of the function being called, except for the return value. Side effects include:

  • Modify any external variables or object properties (for example, global variables or variables in the parent function scope chain)
  • Prints logs to the console
  • Written to the screen
  • Written to the file
  • Written to the network
  • Trigger any external processes
  • Call other functions that have side effects

In functional programming, side effects are generally avoided, which makes the program’s actions easier to understand and test.

Haskell and other functional languages often use Monad to separate and encapsulate side effects from pure functions. The topic of Monad is deep enough for a book, so we’ll talk about it later.

What you need to know now is that side effects need to be isolated from the rest of the software. If you isolate side effects from the rest of the program logic, your software will be easier to extend, refactor, debug, test, and maintain.

This is why most front-end frameworks encourage users to manage state and component rendering in separate, loosely coupled modules.

Reusability through higher-order functions

Functional programming tends to reuse a common set of functional utilities to work with data. Object-oriented programming tends to concentrate methods and data in objects. These collaborative methods can operate only on the data types they are designed to operate on, and generally only on data contained in a particular object instance.

In functional programming, all types of data are equal. The same Map () API can map objects, strings, numbers, or any other data type, because it takes a function as an argument that handles the given data type appropriately. FP uses the general utility technique of higher-order functions to accomplish it.

Functions are first class citizens in JavaScript, and these functions allow, it allows us to treat functions as data — assigning them to variables, passing them to other functions, returning them from functions, and so on.

Higher-order functions are those that take functions as arguments, return functions, or both. Higher-order functions are usually used for:

  • Use callback functions, promises, monad, and so on to abstract or isolate actions, effects, or asynchronous flow control.
  • Create tools that work with multiple data types
  • Apply a function partially to its arguments, or create a Curryized function to reuse or combine functions
  • Gets a list of functions and returns some combination of these input functions

Containers, functors, lists, and flows

Functors can be mapped. In other words, it is a container that has an interface to apply functions to the values within it. When you see the word functor, you should think “mapable.”

We saw earlier that the map() utility works on a variety of data types. It does this using the functor API by promoting the mapping operation. Important flow control operations used by Map () utilize this interface. For array.prototype.map(), the container is an array, but other data structures can also be functors — as long as they provide a mapping API.

Let’s see how array.prototype.map () allows you to extract data types from the mapping utility, making map() available to any data type. We’ll create a simple double() mapping that multiplies any value passed by 2:

const double = n= > n * 2;
const doubleMap = numbers= > numbers.map(double);
console.log(doubleMap([2.3.4])); // [4, 6, 8]
Copy the code

What if we wanted to do something to a target in the game to double the number of points they earned? All we have to do is make some minor changes to the double() function and pass it to map() so that everything still works:

const double = n= > n.points * 2;

const doubleMap = numbers= > numbers.map(double);

console.log(doubleMap([
  { name: 'ball'.points: 2 },
  { name: 'coin'.points: 3 },
  { name: 'candy'.points: 4}]));// [4, 6, 8]
Copy the code

It is important to use abstractions like functors and higher-order functions in order to manipulate any number of different data types using general-purpose utility functions. You will see a similar concept applied in various ways.

“The list represented over time is a stream.”

Now what you need to understand is that arrays and functors are not the only ways in which the concept of containers and the values in containers can be applied. For example, an array is just a list of things. Over time, a list is a stream, so you can use the same type of utility to handle the stream of incoming events-you’ll see a lot of this when you start building real software with FP.

Declarative vs. imperative

Functional programming is a declarative paradigm, meaning that the expression of program logic does not explicitly describe flow control.

Imperative programs spend several lines of code describing a specific step to achieve a desired result — flow control: How to do things.

Declarative programs abstract the flow control process, spending several lines of code describing the data flow: what should be done. How it’s abstracted out.

For example, the imperative map takes an array of numbers and returns a new array where each number is multiplied by 2:

const doubleMap = numbers= > {  
  const doubled = [];
  for (let i = 0; i < numbers.length; i++) {
    doubled.push(numbers[i] * 2);
  }
  return doubled;
};

console.log(doubleMap([2.3.4])); / / [4, 6, 8]
Copy the code

This declarative map does the same thing, but abstracts the flow control out using the array.prototype.map () functional utility, which allows you to represent the data flow more clearly:

const doubleMap = numbers= > numbers.map(n= > n * 2);

console.log(doubleMap([2.3.4])); / / [4, 6, 8]
Copy the code

Imperative code often uses statements. A statement is a piece of code that performs some action. Common statements include for, if, switch, throw, and so on.

Declarative code relies more on expressions. An expression is a piece of code that evaluates a value. An expression is usually a combination of function calls, values, and operators that are used to compute the result.

Here are some examples of expressions:

2 * 2
doubleMap([2, 3, 4])
Math.max(4, 3, 2)
Copy the code

Typically in code, you’ll see expressions assigned to identifiers, returned from functions, or passed to functions. An expression is evaluated before it is allocated, returned, or passed, and actually uses its resulting value.

conclusion

Functional programming tends to:

  • Pure functions rather than state sharing or side effects
  • Immutable rather than mutable data
  • Compositional functions instead of imperative flow control
  • A number of general-purpose, reusable utilities that use higher-order functions to work with multiple data types, rather than methods that operate only on data in the same location
  • Declarative rather than imperative code (what rather than how)
  • Expressions instead of statements
  • Containers and higher-order functions instead of polymorphism

homework

Learn and practice the core functions of these functional arrays:

  • .map()
  • .filter()
  • .reduce()

Explore the Mastering JavaScript Interview series

  • What is a closure?
  • What’s the difference between class and stereotype inheritance
  • What is a pure function?
  • What is the composition function?
  • What is functional programming?
  • What is Promise?
  • Soft skills

This post was included in the book “Composing Software. Buy the book | Index | < Previous | Next >


Eric Elliott is an expert on distributed systems and the author of Two books, Composing Software and Writing JavaScript Programs. As co-founder of EricElliottJS.com and DevSogo.io, he teaches developers the skills they need to work remotely and achieve work-life balance. He created and advised the encryption project’s development team. He has also contributed software experiences to Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC, and top recording artists including Usher, Frank Ocean, And Metallica.

He enjoys a remote lifestyle with some of the most beautiful women in the world.

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.