02 How to Manage Program State Changes Through Closures

02 How to Manage Program State Changes Through Closures #

Hello, I’m Ishikawa.

From our previous lesson, we now know that side effects exist in functional programming, and pure functions and immutability are the two core concepts in reducing side effects. In theory, if we want to minimize side effects to almost zero, we can use pure functions that do not accept any parameters. However, such completely self-contained functions would have little practical use.

Therefore, as a function, it still needs inputs, computations, and outputs in order to interact with the outside world and make our system “alive”. And a living system’s state is certainly changing constantly. So, how can we manage this change while adhering to the principle of immutability?

In today’s lesson, we will explore which values are mutable and which are immutable in functional programming, as well as how to achieve immutability while updating state.

Immutable (Immutability) #

First, let’s clarify a question: Are values mutable or immutable? In JavaScript, values are generally divided into two types: primitive types and object types.

Let’s start with primitive types. Data types like strings or numbers belong to primitive types, and by nature, they are immutable. For example, if we input 2 = 2.5 in console.log, we will get an “invalid” result, which proves that we cannot change the value of a primitive type.

2 = 2.5 // invalid

Next, let’s talk about object types. In JavaScript, data types like arrays and objects are considered object types. These types of data are more like data structures or containers. Are these “values” mutable? By looking at the example of arrays from the previous lesson, you can see that these values are mutable, for example, through methods like splice.

So, let’s now explore how to achieve immutability when updating state using object types.

Props and State in React.js #

Here, we’ll take React.js as an example to observe what types of values it uses for state.

When it comes to state, React has two important concepts that are often confused: props and state. Props are usually passed as external parameters into a function and then rendered as static elements in the UI. State is an internal variable that is rendered as a dynamic element in the UI and is updated based on behavior.

In the diagram above, there is a static text and a dynamic counter. In this case, props represent the text “Click to increase:”, which should generally remain unchanged on the page. It is a fixed prompt, so it is considered props, a static “property”.

On the other hand, the value of the counter button is incremented with each click, which means it changes dynamically based on the click. Therefore, we call it state, a dynamic “state”.

// Props
class Instruction extends React.Component {
  render() {
    return <span>Instruction - {this.props.message}</span>;
  }
}
const element = <Instruction message="Click to increase:" />;

// State
class Button extends React.Component {
  constructor() {
    super();
    this.state = {
      count: 0,
    };
  }
  updateCount() {}
  render() {
    return (<button onClick={() => this.updateCount()}> Clicked {this.state.count} times</button>);
  }
}

Now, let’s go back to the previous question: in React.js, what types of values are props and state? The answer is objects. Props and state are both used to store state as objects.

However, why does React use objects to store values for props and state? Does it have any other options? Let’s find out next.

Immutable Structured Values #

Let’s first consider a question: are props and state necessary?

The answer is that props are necessary, while state is not. Because if our page is a static page, we just need to render the content, but this purely static application cannot interact with users.

So in frontend development, a purely static application is called dumb as f*ck. Of course, this is not a very civilized term, but it’s straightforward and its meaning is obvious, which is that such an application is too “stupid”.

Our application definitely needs to interact with users, and once there is interaction, we need to manage the state of values and design a series of behaviors around those values. In this process, we need to consider the issue of structural immutability of values.

So next, let’s take a look at the data types available for structural operations on values.

Closures and Objects #

First, closures and objects can both encapsulate a state value and create behaviors.

The biggest feature of a closure is that it can break the limitations of lifetime and scope, which refers to the control of time and space.

Breaking the limitation of lifetime means that when an inner function is nested inside an outer function and the inner function references a variable from the outer function, that variable will break the limitation of lifetime and still exist after the function finishes executing. For example, in the closure example below, we create a counter that increments by 1 and remembers the previous value.

Breaking the limitation of scope means that we can return an inner function as a method and call it externally. For example, in the following code, the method counting returned by counter can be executed through counter1, which breaks the limitation of the scope of counter.

function counter() {
    let name = "计数";
    let curVal = 0;
    function counting() {
        curVal++;
    }
    function getCount() {
        console.log(
            `${name}${curVal}`
        );
    }
    return {counting,getCount}
}

var counter1 = counter();

counter1.counting();  
counter1.counting();  
counter1.counting();  
counter1.getCount();  // 计数是3

Similarly, we can encapsulate a state through objects and create a method that acts on this state value.

var counter = {
    name: "计数",
    curVal: 0,
    counting() {
        this.curVal++;
        console.log(
            `${this.name}${this.curVal}`
        );
    }
};

counter.counting(); // 计数是1
counter.counting(); // 计数是2
counter.counting(); // 计数是3

So, purely from the perspective of managing the state of values and a series of behaviors surrounding them, we can say that closures and objects are isomorphic, meaning they can play similar roles. For example, the state in the closure example is the property in the object, and the behaviors we create for values in closures can also be implemented through methods in objects.

图片

You may wonder, what is the significance of comparing them? In fact, they have differences in terms of privacy, state cloning, and performance, which have advantages and disadvantages in dealing with values structurally.

Next, let’s examine these differences from the perspectives of privacy, state cloning, and performance.

图片

Property Access and Modification #

In fact, you can discover from the closure example that unless it is accessed through an interface, i.e. returning an inner function in the outer function, such as the method getCount used to get the value or the method counting used to reassign the value, the internal value is not visible to the outside.

Therefore, it can control the properties we want to expose or hide, as well as related operations, with fine granularity.

counter1.counting();  
counter1.getCount();  

Objects, on the other hand, are different. We can access and reassign properties in an object without any special approach. If we want to follow the principle of immutability, there is a method called Object.freeze(), which can make all properties of an object read-only with writable: false. But there is one thing to note here. Using freeze makes all properties of the object read-only and irreversible. Of course, the benefit is that it strictly adheres to the principle of immutability.

counter.name; 
counter.initVal; 
counter.counting();

Copying State #

So far, we can see that for primitive types of data, there is no need to worry too much about the immutability of values.

However, since an application can have a “series” of states, we usually need to save a “series” of states using data structures such as arrays and objects. In the face of this type of data, how can we follow the principle of immutability?

  • How can we manage state through copying?

To solve this problem, we can use the approach of copying + updating. In other words, instead of modifying the original object and array values, we make a copy and then make changes to the copied version.

For example, in the following example, we use the spread operator to spread the elements of the array and object and assign those elements to new variables. Through this approach, we achieve shallow copying. When we look at the original array and object afterwards, we will find that the original data remains unchanged.

// Shallow copying of an array
var a = [ 1, 2 ];
var b = [ ...a ];
b.push( 3 );
a;  // [1,2]
b;  // [1,2,3]

// Shallow copying of an object
var o = {
    x: 1,
    y: 2
};
var p = { ...o };
p.y = 3; 
o.y;  // 2
p.y;  // 3

So it can be seen that arrays and objects are easy to copy, while closures are relatively more difficult to copy.

  • How do we solve the performance issue with copying?

From the above examples, we can see that although we can achieve immutability through copying state, there is a performance issue that comes along with it.

If a value only changes once or twice, it’s not a problem. But let’s say there are values constantly changing in our system, if we copy the value every time, it will consume a large amount of memory. In this case, how do we handle it?

In fact, in this situation, there is a solution which is similar to a linked list structure, where there is a group of objects that record the indices of changes and the respective values.

For example, in the following array [3, 1, 0, 7], if we change the value at index 0 to 2, the value at index 3 to 6, and add 1 to the end, we would get [2, 1, 0, 6, 1]. If we only record the changes, it would be 0:2, 3:6, and 4:1. This way, a lot of memory usage is reduced.

Image

In fact, there are already many mature third-party libraries on the market, such as immutable.js, which have their own data structures, such as array list and object map, as well as related algorithms to solve similar problems.

Consideration for Performance #

Next, let’s take a look at the performance considerations.

From a performance point of view, the memory and operations of objects are usually better than closures. For example, in the first closure example below, we create a new function expression every time we use it.

In the second object example, we bind this to greetings2 using bind(). This way, PrintMessageB will refer to greetings2.name as this.name, achieving the same effect as a closure. But we don’t need to create a closure, we just need to point this to the referenced object.

// Closure
function PrintMessageA(name) {
    return function printName(){
        return `${name}, 你好!`;
    };
}
var greetings1 = PrintMessageA( "先生" );
greetings1();  // 先生,你好!

// Object
function PrintMessageB(){
   return `${this.name}, 你好!`;
}
var greetings2 = PrintMessageB.bind( {
    name: "先生"
} );
greetings2();  // 先生,你好!

Summary #

In this lesson, we have delved deep into the immutability concept in functional programming. We need to pay particular attention to the different advantages of objects and closures in handling immutability.

  • In terms of privacy of properties and methods, closures naturally protect properties and they can also selectively expose interfaces to achieve finer-grained access or reassignment of states. However, it seems that closures have little relevance to the problem we are trying to solve.
  • On the other hand, objects not only easily achieve overall immutability of props, but also have advantages in copying when state changes are needed. However, from a performance perspective, if the amount of copying is small, their performance might be similar. But in a high-frequency interactive interface, even slight differences can be magnified.

In conclusion, in React.js, it chooses to use objects as the value types for props and state, which makes it easier to ensure the overall immutability of properties and states. Additionally, it is easier to handle state changes with object copying. Performance will also be better when dealing with high-frequency interactions.

On the other hand, closures have advantages in privacy and finer-grained operations, but they have no practical use in application interactions and state management scenarios. Therefore, the conditions favoring the use of objects will be relatively more common.

Finally, you can review the advantages and disadvantages of these two approaches. In fact, based on examples in React.js, you can discover that there is no absolute good or bad for different data types and ways of handling changes and immutability. It depends on the specific situation to determine which approach is more suitable for your program and the scenarios that your application needs to support.

Image

Thought Questions #

When we talk about the state copying by value, we say that spread achieves shallow copying. Do you understand the corresponding deep copying? Will it affect state management?

Feel free to share your thoughts and answers in the comment section, and also feel free to share today’s content with more friends.

Further Reading #